Completed
Pull Request — master (#84)
by
unknown
05:28
created

CliMenuBuilder::setMargin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Action\ExitAction;
6
use PhpSchool\CliMenu\Action\GoBackAction;
7
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
8
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
9
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
10
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
11
use PhpSchool\CliMenu\MenuItem\SelectableItem;
12
use PhpSchool\CliMenu\MenuItem\SplitItem;
13
use PhpSchool\CliMenu\MenuItem\StaticItem;
14
use PhpSchool\CliMenu\Terminal\TerminalFactory;
15
use PhpSchool\CliMenu\Util\ColourUtil;
16
use PhpSchool\Terminal\Terminal;
17
use RuntimeException;
18
19
/**
20
 * @author Michael Woodward <[email protected]>
21
 * @author Aydin Hassan <[email protected]>
22
 */
23
class CliMenuBuilder
24
{
25
    /**
26
     * @var bool
27
     */
28
    private $isBuilt = false;
29
30
    /**
31
     * @var null|self
32
     */
33
    private $parent;
34
35
    /**
36
     * @var self[]
37
     */
38
    private $subMenuBuilders = [];
39
40
    /**
41
     * @var CliMenu[]
42
     */
43
    private $subMenus = [];
44
45
    /**
46
     * @var string
47
     */
48
    private $goBackButtonText = 'Go Back';
49
50
    /**
51
     * @var string
52
     */
53
    private $exitButtonText = 'Exit';
54
55
    /**
56
     * @var array
57
     */
58
    private $menuItems = [];
59
60
    /**
61
     * @var array
62
     */
63
    private $style;
64
65
    /**
66
     * @var Terminal
67
     */
68
    private $terminal;
69
70
    /**
71
     * @var string
72
     */
73
    private $menuTitle = null;
74
75
    /**
76
     * @var bool
77
     */
78
    private $disableDefaultItems = false;
79
80
    /**
81
     * @var bool
82
     */
83
    private $disabled = false;
84
85
    public function __construct(CliMenuBuilder $parent = null)
86
    {
87
        $this->parent   = $parent;
88
        $this->terminal = $this->parent !== null
89
            ? $this->parent->getTerminal()
90
            : TerminalFactory::fromSystem();
91
        $this->style    = MenuStyle::getDefaultStyleValues();
92
    }
93
94
    public function setTitle(string $title) : self
95
    {
96
        $this->menuTitle = $title;
97
98
        return $this;
99
    }
100
101
    public function addMenuItem(MenuItemInterface $item) : self
102
    {
103
        $this->menuItems[] = $item;
104
105
        return $this;
106
    }
107
108
    public function addItem(
109
        string $text,
110
        callable $itemCallable,
111
        bool $showItemExtra = false,
112
        bool $disabled = false
113
    ) : self {
114
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
115
116
        return $this;
117
    }
118
119
    public function addItems(array $items) : self
120
    {
121
        foreach ($items as $item) {
122
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to addItem() misses a required argument $itemCallable.

This check looks for function calls that miss required arguments.

Loading history...
123
        }
124
125
        return $this;
126
    }
127
128
    public function addStaticItem(string $text) : self
129
    {
130
        $this->addMenuItem(new StaticItem($text));
131
132
        return $this;
133
    }
134
135
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
136
    {
137
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
138
139
        return $this;
140
    }
141
142
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
143
    {
144
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
145
146
        return $this;
147
    }
148
149
    /**
150
     * Add a submenu with a string identifier
151
     */
152
    public function addSubMenu(string $id, CliMenuBuilder $subMenuBuilder = null) : CliMenuBuilder
153
    {
154
        $this->menuItems[]  = $id;
155
156
        if (null === $subMenuBuilder) {
157
            $this->subMenuBuilders[$id] = new static($this);
158
            return $this->subMenuBuilders[$id];
159
        }
160
161
        $this->subMenuBuilders[$id] = $subMenuBuilder;
162
        return $this;
163
    }
164
165
    /**
166
     * Add a split item
167
     */
168
    public function addSplitItem() : SplitItem
169
    {
170
        $splitItem = new SplitItem($this);
171
        $this->addMenuItem($splitItem);
172
173
        return $splitItem;
174
    }
175
176
    /**
177
     * Disable a submenu
178
     *
179
     * @throws \InvalidArgumentException
180
     */
181
    public function disableMenu() : self
182
    {
183
        if (!$this->parent) {
184
            throw new \InvalidArgumentException(
185
                'You can\'t disable the root menu'
186
            );
187
        }
188
189
        $this->disabled = true;
190
191
        return $this;
192
    }
193
194
    public function isMenuDisabled() : bool
195
    {
196
        return $this->disabled;
197
    }
198
199
    public function setGoBackButtonText(string $goBackButtonTest) : self
200
    {
201
        $this->goBackButtonText = $goBackButtonTest;
202
203
        return $this;
204
    }
205
206
    public function setExitButtonText(string $exitButtonText) : self
207
    {
208
        $this->exitButtonText = $exitButtonText;
209
210
        return $this;
211
    }
212
213
    public function setBackgroundColour(string $colour, string $fallback = null) : self
214
    {
215
        $this->style['bg'] = ColourUtil::validateColour(
216
            $this->terminal,
217
            $colour,
218
            $fallback
219
        );
220
221
        return $this;
222
    }
223
224 View Code Duplication
    public function setForegroundColour(string $colour, string $fallback = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225
    {
226
        $this->style['fg'] = ColourUtil::validateColour(
227
            $this->terminal,
228
            $colour,
229
            $fallback
230
        );
231
232
        return $this;
233
    }
234
235
    public function setWidth(int $width) : self
236
    {
237
        $this->style['width'] = $width;
238
239
        return $this;
240
    }
241
242 View Code Duplication
    public function setPadding(int $topBottom, int $leftRight = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
243
    {
244
        if ($leftRight === null) {
245
            $leftRight = $topBottom;
246
        }
247
248
        $this->setPaddingTopBottom($topBottom);
249
        $this->setPaddingLeftRight($leftRight);
250
251
        return $this;
252
    }
253
254
    public function setPaddingTopBottom(int $topBottom) : self
255
    {
256
        $this->style['paddingTopBottom'] = $topBottom;
257
258
        return $this;
259
    }
260
261
    public function setPaddingLeftRight(int $leftRight) : self
262
    {
263
        $this->style['paddingLeftRight'] = $leftRight;
264
265
        return $this;
266
    }
267
268
    public function setMarginAuto() : self
269
    {
270
        $this->style['marginAuto'] = true;
271
272
        return $this;
273
    }
274
275
    public function setMargin(int $margin) : self
276
    {
277
        $this->style['marginAuto'] = false;
278
        $this->style['margin'] = $margin;
279
280
        return $this;
281
    }
282
283
    public function setUnselectedMarker(string $marker) : self
284
    {
285
        $this->style['unselectedMarker'] = $marker;
286
287
        return $this;
288
    }
289
290
    public function setSelectedMarker(string $marker) : self
291
    {
292
        $this->style['selectedMarker'] = $marker;
293
294
        return $this;
295
    }
296
297
    public function setItemExtra(string $extra) : self
298
    {
299
        $this->style['itemExtra'] = $extra;
300
301
        return $this;
302
    }
303
304
    public function setTitleSeparator(string $separator) : self
305
    {
306
        $this->style['titleSeparator'] = $separator;
307
308
        return $this;
309
    }
310
311
    public function setBorder(
312
        int $topWidth,
313
        $rightWidth = null,
314
        $bottomWidth = null,
315
        $leftWidth = null,
316
        string $colour = null
317
    ) : self {
318 View Code Duplication
        if (!is_int($rightWidth)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
            $colour = $rightWidth;
320
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
321
        } elseif (!is_int($bottomWidth)) {
322
            $colour = $bottomWidth;
323
            $bottomWidth = $topWidth;
324
            $leftWidth = $rightWidth;
325
        } elseif (!is_int($leftWidth)) {
326
            $colour = $leftWidth;
327
            $leftWidth = $rightWidth;
328
        }
329
330
        $this->style['borderTopWidth'] = $topWidth;
331
        $this->style['borderRightWidth'] = $rightWidth;
332
        $this->style['borderBottomWidth'] = $bottomWidth;
333
        $this->style['borderLeftWidth'] = $leftWidth;
334
335
        if (is_string($colour)) {
336
            $this->style['borderColour'] = $colour;
337
        } elseif ($colour !== null) {
338
            throw new \InvalidArgumentException('Invalid colour');
339
        }
340
341
        return $this;
342
    }
343
344
    public function setTerminal(Terminal $terminal) : self
345
    {
346
        $this->terminal = $terminal;
347
        return $this;
348
    }
349
350
    public function getTerminal() : Terminal
351
    {
352
        return $this->terminal;
353
    }
354
355
    private function getDefaultItems() : array
356
    {
357
        $actions = [];
358
        if ($this->parent) {
359
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
360
        }
361
362
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
363
        return $actions;
364
    }
365
366
    public function disableDefaultItems() : self
367
    {
368
        $this->disableDefaultItems = true;
369
370
        return $this;
371
    }
372
373
    private function itemsHaveExtra(array $items) : bool
374
    {
375
        return !empty(array_filter($items, function (MenuItemInterface $item) {
376
            return $item->showsItemExtra();
377
        }));
378
    }
379
380
    /**
381
     * Recursively drop back to the parents menu style
382
     * when the current menu has a parent and has no changes
383
     */
384
    private function getMenuStyle() : MenuStyle
385
    {
386
        if (null === $this->parent) {
387
            return $this->buildStyle();
388
        }
389
390
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
391
            return $this->buildStyle();
392
        }
393
394
        return $this->parent->getMenuStyle();
395
    }
396
397
    private function buildStyle() : MenuStyle
398
    {
399
        $style = (new MenuStyle($this->terminal))
400
            ->setFg($this->style['fg'])
401
            ->setBg($this->style['bg'])
402
            ->setWidth($this->style['width'])
403
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
404
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
405
            ->setSelectedMarker($this->style['selectedMarker'])
406
            ->setUnselectedMarker($this->style['unselectedMarker'])
407
            ->setItemExtra($this->style['itemExtra'])
408
            ->setDisplaysExtra($this->style['displaysExtra'])
409
            ->setTitleSeparator($this->style['titleSeparator'])
410
            ->setBorderTopWidth($this->style['borderTopWidth'])
411
            ->setBorderRightWidth($this->style['borderRightWidth'])
412
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
413
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
414
            ->setBorderColour($this->style['borderColour']);
415
416
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
417
418
        return $style;
419
    }
420
421
    /**
422
     * Return to parent builder
423
     *
424
     * @throws RuntimeException
425
     */
426
    public function end() : CliMenuBuilder
427
    {
428
        if (null === $this->parent) {
429
            throw new RuntimeException('No parent builder to return to');
430
        }
431
432
        return $this->parent;
433
    }
434
435
    /**
436
     * @throws RuntimeException
437
     */
438
    public function getSubMenu(string $id) : CliMenu
439
    {
440
        if (false === $this->isBuilt) {
441
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
442
        }
443
444
        return $this->subMenus[$id];
445
    }
446
447
    private function buildSubMenus(array $items) : array
448
    {
449
        return array_map(function ($item) {
450
            if (!is_string($item)) {
451
                return $item;
452
            }
453
454
            $menuBuilder           = $this->subMenuBuilders[$item];
455
            $this->subMenus[$item] = $menuBuilder->build();
456
457
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
458
        }, $items);
459
    }
460
461
    public function build() : CliMenu
462
    {
463
        $this->isBuilt = true;
464
465
        $mergedItems = $this->disableDefaultItems
466
            ? $this->menuItems
467
            : array_merge($this->menuItems, $this->getDefaultItems());
468
469
        $menuItems = $this->buildSubMenus($mergedItems);
470
471
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
472
473
        $menu = new CliMenu(
474
            $this->menuTitle,
475
            $menuItems,
476
            $this->terminal,
477
            $this->getMenuStyle()
478
        );
479
480
        foreach ($this->subMenus as $subMenu) {
481
            $subMenu->setParent($menu);
482
        }
483
484
        return $menu;
485
    }
486
}
487