Completed
Pull Request — master (#125)
by
unknown
02:10
created

CliMenuBuilder::addSplitItem()   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 0
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
    private $previousBuilder = null;
36
37
    /**
38
     * @var self[]
39
     */
40
    private $subMenuBuilders = [];
41
42
    /**
43
     * @var CliMenu[]
44
     */
45
    private $subMenus = [];
46
47
    /**
48
     * @var string
49
     */
50
    private $goBackButtonText = 'Go Back';
51
52
    /**
53
     * @var string
54
     */
55
    private $exitButtonText = 'Exit';
56
57
    /**
58
     * @var array
59
     */
60
    private $menuItems = [];
61
62
    /**
63
     * @var array
64
     */
65
    private $style;
66
67
    /**
68
     * @var Terminal
69
     */
70
    private $terminal;
71
72
    /**
73
     * @var string
74
     */
75
    private $menuTitle = null;
76
77
    /**
78
     * @var bool
79
     */
80
    private $disableDefaultItems = false;
81
82
    /**
83
     * @var bool
84
     */
85
    private $disabled = false;
86
87
    public function __construct(CliMenuBuilder $parent = null, $previousBuilder = null)
88
    {
89
        $this->parent   = $parent;
90
        $this->previousBuilder = $previousBuilder;
91
        $this->terminal = $this->parent !== null
92
            ? $this->parent->getTerminal()
93
            : TerminalFactory::fromSystem();
94
        $this->style    = MenuStyle::getDefaultStyleValues();
95
    }
96
97
    public function setTitle(string $title) : self
98
    {
99
        $this->menuTitle = $title;
100
101
        return $this;
102
    }
103
104
    public function addMenuItem(MenuItemInterface $item) : self
105
    {
106
        $this->menuItems[] = $item;
107
108
        return $this;
109
    }
110
111
    public function addItem(
112
        string $text,
113
        callable $itemCallable,
114
        bool $showItemExtra = false,
115
        bool $disabled = false
116
    ) : self {
117
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
118
119
        return $this;
120
    }
121
122
    public function addItems(array $items) : self
123
    {
124
        foreach ($items as $item) {
125
            $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...
126
        }
127
128
        return $this;
129
    }
130
131
    public function addStaticItem(string $text) : self
132
    {
133
        $this->addMenuItem(new StaticItem($text));
134
135
        return $this;
136
    }
137
138
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
139
    {
140
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
141
142
        return $this;
143
    }
144
145
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
146
    {
147
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
148
149
        return $this;
150
    }
151
152
    /**
153
     * Add a submenu with a string identifier
154
     */
155
    public function addSubMenu(string $id, CliMenuBuilder $subMenuBuilder = null) : CliMenuBuilder
156
    {
157
        $this->menuItems[]  = $id;
158
159
        if (null === $subMenuBuilder) {
160
            $this->subMenuBuilders[$id] = new static($this);
161
            return $this->subMenuBuilders[$id];
162
        }
163
164
        $this->subMenuBuilders[$id] = $subMenuBuilder;
165
        return $this;
166
    }
167
168
    /**
169
     * Injects a submenu directly (without going through the builder
170
     */
171
    public function injectSubMenu(string $id, CliMenu $subMenu) : CliMenuBuilder
172
    {
173
        $this->subMenus[$id] = $subMenu;
174
175
        return $this;
176
    }
177
178
    /**
179
     * Add a split item
180
     */
181
    public function addSplitItem() : SplitItem
182
    {
183
        $splitItem = new SplitItem($this);
184
        $this->addMenuItem($splitItem);
185
186
        return $splitItem;
187
    }
188
189
    /**
190
     * Disable a submenu
191
     *
192
     * @throws \InvalidArgumentException
193
     */
194
    public function disableMenu() : self
195
    {
196
        if (!$this->parent) {
197
            throw new \InvalidArgumentException(
198
                'You can\'t disable the root menu'
199
            );
200
        }
201
202
        $this->disabled = true;
203
204
        return $this;
205
    }
206
207
    public function isMenuDisabled() : bool
208
    {
209
        return $this->disabled;
210
    }
211
212
    public function setGoBackButtonText(string $goBackButtonTest) : self
213
    {
214
        $this->goBackButtonText = $goBackButtonTest;
215
216
        return $this;
217
    }
218
219
    public function setExitButtonText(string $exitButtonText) : self
220
    {
221
        $this->exitButtonText = $exitButtonText;
222
223
        return $this;
224
    }
225
226
    public function setBackgroundColour(string $colour, string $fallback = null) : self
227
    {
228
        $this->style['bg'] = ColourUtil::validateColour(
229
            $this->terminal,
230
            $colour,
231
            $fallback
232
        );
233
234
        return $this;
235
    }
236
237 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...
238
    {
239
        $this->style['fg'] = ColourUtil::validateColour(
240
            $this->terminal,
241
            $colour,
242
            $fallback
243
        );
244
245
        return $this;
246
    }
247
248
    public function setWidth(int $width) : self
249
    {
250
        $this->style['width'] = $width;
251
252
        return $this;
253
    }
254
255 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...
256
    {
257
        if ($leftRight === null) {
258
            $leftRight = $topBottom;
259
        }
260
261
        $this->setPaddingTopBottom($topBottom);
262
        $this->setPaddingLeftRight($leftRight);
263
264
        return $this;
265
    }
266
267
    public function setPaddingTopBottom(int $topBottom) : self
268
    {
269
        $this->style['paddingTopBottom'] = $topBottom;
270
271
        return $this;
272
    }
273
274
    public function setPaddingLeftRight(int $leftRight) : self
275
    {
276
        $this->style['paddingLeftRight'] = $leftRight;
277
278
        return $this;
279
    }
280
281
    public function setMarginAuto() : self
282
    {
283
        $this->style['marginAuto'] = true;
284
285
        return $this;
286
    }
287
288
    public function setMargin(int $margin) : self
289
    {
290
        $this->style['marginAuto'] = false;
291
        $this->style['margin'] = $margin;
292
293
        return $this;
294
    }
295
296
    public function setUnselectedMarker(string $marker) : self
297
    {
298
        $this->style['unselectedMarker'] = $marker;
299
300
        return $this;
301
    }
302
303
    public function setSelectedMarker(string $marker) : self
304
    {
305
        $this->style['selectedMarker'] = $marker;
306
307
        return $this;
308
    }
309
310
    public function setItemExtra(string $extra) : self
311
    {
312
        $this->style['itemExtra'] = $extra;
313
314
        return $this;
315
    }
316
317
    public function setTitleSeparator(string $separator) : self
318
    {
319
        $this->style['titleSeparator'] = $separator;
320
321
        return $this;
322
    }
323
324
    public function setBorder(
325
        int $topWidth,
326
        $rightWidth = null,
327
        $bottomWidth = null,
328
        $leftWidth = null,
329
        string $colour = null
330
    ) : self {
331 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...
332
            $colour = $rightWidth;
333
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
334
        } elseif (!is_int($bottomWidth)) {
335
            $colour = $bottomWidth;
336
            $bottomWidth = $topWidth;
337
            $leftWidth = $rightWidth;
338
        } elseif (!is_int($leftWidth)) {
339
            $colour = $leftWidth;
340
            $leftWidth = $rightWidth;
341
        }
342
343
        $this->style['borderTopWidth'] = $topWidth;
344
        $this->style['borderRightWidth'] = $rightWidth;
345
        $this->style['borderBottomWidth'] = $bottomWidth;
346
        $this->style['borderLeftWidth'] = $leftWidth;
347
348
        if (is_string($colour)) {
349
            $this->style['borderColour'] = $colour;
350
        } elseif ($colour !== null) {
351
            throw new \InvalidArgumentException('Invalid colour');
352
        }
353
354
        return $this;
355
    }
356
357
    public function setBorderTopWidth(int $width) : self
358
    {
359
        $this->style['borderTopWidth'] = $width;
360
        
361
        return $this;
362
    }
363
364
    public function setBorderRightWidth(int $width) : self
365
    {
366
        $this->style['borderRightWidth'] = $width;
367
368
        return $this;
369
    }
370
371
    public function setBorderBottomWidth(int $width) : self
372
    {
373
        $this->style['borderBottomWidth'] = $width;
374
375
        return $this;
376
    }
377
378
    public function setBorderLeftWidth(int $width) : self
379
    {
380
        $this->style['borderLeftWidth'] = $width;
381
382
        return $this;
383
    }
384
385
    public function setBorderColour(string $colour, $fallback = null) : self
386
    {
387
        $this->style['borderColour'] = $colour;
388
        $this->style['borderColourFallback'] = $fallback;
389
390
        return $this;
391
    }
392
393
    public function setTerminal(Terminal $terminal) : self
394
    {
395
        $this->terminal = $terminal;
396
        return $this;
397
    }
398
399
    public function getTerminal() : Terminal
400
    {
401
        return $this->terminal;
402
    }
403
404
    private function getDefaultItems() : array
405
    {
406
        $actions = [];
407
        if ($this->parent) {
408
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
409
        }
410
411
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
412
        return $actions;
413
    }
414
415
    public function disableDefaultItems() : self
416
    {
417
        $this->disableDefaultItems = true;
418
419
        return $this;
420
    }
421
422
    private function itemsHaveExtra(array $items) : bool
423
    {
424
        return !empty(array_filter($items, function (MenuItemInterface $item) {
425
            return $item->showsItemExtra();
426
        }));
427
    }
428
429
    /**
430
     * Recursively drop back to the parents menu style
431
     * when the current menu has a parent and has no changes
432
     */
433
    private function getMenuStyle() : MenuStyle
434
    {
435
        if (null === $this->parent) {
436
            return $this->buildStyle();
437
        }
438
439
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
440
            return $this->buildStyle();
441
        }
442
443
        return $this->parent->getMenuStyle();
444
    }
445
446
    private function buildStyle() : MenuStyle
447
    {
448
        $style = (new MenuStyle($this->terminal))
449
            ->setFg($this->style['fg'])
450
            ->setBg($this->style['bg'])
451
            ->setWidth($this->style['width'])
452
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
453
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
454
            ->setSelectedMarker($this->style['selectedMarker'])
455
            ->setUnselectedMarker($this->style['unselectedMarker'])
456
            ->setItemExtra($this->style['itemExtra'])
457
            ->setDisplaysExtra($this->style['displaysExtra'])
458
            ->setTitleSeparator($this->style['titleSeparator'])
459
            ->setBorderTopWidth($this->style['borderTopWidth'])
460
            ->setBorderRightWidth($this->style['borderRightWidth'])
461
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
462
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
463
            ->setBorderColour($this->style['borderColour'], $this->style['borderColourFallback']);
464
465
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
466
467
        return $style;
468
    }
469
470
    /**
471
     * Return to parent builder
472
     *
473
     * @throws RuntimeException
474
     */
475
    public function end()
476
    {
477
        if (null !== $this->previousBuilder) {
478
            return $this->previousBuilder;
479
        }
480
481
        if (null === $this->parent) {
482
            throw new RuntimeException('No parent builder to return to');
483
        }
484
485
        return $this->parent;
486
    }
487
488
    /**
489
     * @throws RuntimeException
490
     */
491
    public function getSubMenu(string $id) : CliMenu
492
    {
493
        if (false === $this->isBuilt) {
494
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
495
        }
496
497
        return $this->subMenus[$id];
498
    }
499
500
    private function buildSubMenus(array $items) : array
501
    {
502
        return array_map(function ($item) {
503
            if (!is_string($item)) {
504
                return $item;
505
            }
506
507
            $menuBuilder           = $this->subMenuBuilders[$item];
508
            $this->subMenus[$item] = $menuBuilder->build();
509
510
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
511
        }, $items);
512
    }
513
514
    public function build() : CliMenu
515
    {
516
        $this->isBuilt = true;
517
518
        $mergedItems = $this->disableDefaultItems
519
            ? $this->menuItems
520
            : array_merge($this->menuItems, $this->getDefaultItems());
521
522
        $menuItems = $this->buildSubMenus($mergedItems);
523
524
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
525
526
        $menu = new CliMenu(
527
            $this->menuTitle,
528
            $menuItems,
529
            $this->terminal,
530
            $this->getMenuStyle()
531
        );
532
533
        foreach ($this->subMenus as $subMenu) {
534
            $subMenu->setParent($menu);
535
        }
536
537
        return $menu;
538
    }
539
}
540