Passed
Pull Request — master (#203)
by
unknown
01:51
created

CliMenuBuilder::setBorderTopWidth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpSchool\CliMenu\Builder;
4
5
use Closure;
6
use PhpSchool\CliMenu\Action\ExitAction;
7
use PhpSchool\CliMenu\Action\GoBackAction;
8
use PhpSchool\CliMenu\Exception\InvalidShortcutException;
9
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
10
use PhpSchool\CliMenu\MenuItem\CheckableItem;
11
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
12
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
13
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
14
use PhpSchool\CliMenu\MenuItem\RadioItem;
15
use PhpSchool\CliMenu\MenuItem\SelectableInterface;
16
use PhpSchool\CliMenu\MenuItem\SelectableItem;
17
use PhpSchool\CliMenu\CliMenu;
18
use PhpSchool\CliMenu\MenuItem\SplitItem;
19
use PhpSchool\CliMenu\MenuItem\StaticItem;
20
use PhpSchool\CliMenu\MenuStyle;
21
use PhpSchool\CliMenu\Style\CheckableStyle;
22
use PhpSchool\CliMenu\Style\RadioStyle;
23
use PhpSchool\CliMenu\Style\SelectableStyle;
24
use PhpSchool\CliMenu\Terminal\TerminalFactory;
25
use PhpSchool\Terminal\Terminal;
26
27
/**
28
 * @author Michael Woodward <[email protected]>
29
 * @author Aydin Hassan <[email protected]>
30
 */
31
class CliMenuBuilder
32
{
33
    /**
34
     * @var CliMenu
35
     */
36
    private $menu;
37
38
    /**
39
     * @var string
40
     */
41
    private $goBackButtonText = 'Go Back';
42
43
    /**
44
     * @var string
45
     */
46
    private $exitButtonText = 'Exit';
47
48
    /**
49
     * @var MenuStyle
50
     */
51
    private $style;
52
53
    /**
54
     * @var Terminal
55
     */
56
    private $terminal;
57
58
    /**
59
     * @var bool
60
     */
61
    private $disableDefaultItems = false;
62
63
    /**
64
     * @var bool
65
     */
66
    private $disabled = false;
67
68
    /**
69
     * Whether or not to auto create keyboard shortcuts for items
70
     * when they contain square brackets. Eg: [M]y item
71
     *
72
     * @var bool
73
     */
74
    private $autoShortcuts = false;
75
76
    /**
77
     * Regex to auto match for shortcuts defaults to looking
78
     * for a single character encased in square brackets
79
     *
80
     * @var string
81
     */
82
    private $autoShortcutsRegex = '/\[(.)\]/';
83
84
    /**
85
     * @var bool
86
     */
87
    private $subMenu = false;
88
89
    public function __construct(Terminal $terminal = null)
90
    {
91
        $this->terminal = $terminal ?? TerminalFactory::fromSystem();
92
        $this->style    = new MenuStyle($this->terminal);
93
        $this->menu     = new CliMenu(null, [], $this->terminal, $this->style);
94
    }
95
    
96
    public static function newSubMenu(Terminal $terminal) : self
97
    {
98
        $instance = new self($terminal);
99
        $instance->subMenu = true;
100
        
101
        return $instance;
102
    }
103
104
    public function setTitle(string $title) : self
105
    {
106
        $this->menu->setTitle($title);
107
108
        return $this;
109
    }
110
111
    public function addMenuItem(MenuItemInterface $item) : self
112
    {
113
        $this->menu->addItem($item);
114
115
        $this->processItemShortcut($item);
116
117
        return $this;
118
    }
119
120
    public function addItem(
121
        string $text,
122
        callable $itemCallable,
123
        bool $showItemExtra = false,
124
        bool $disabled = false
125
    ) : self {
126
        $item = (new SelectableItem($text, $itemCallable, $showItemExtra, $disabled))
127
            ->setStyle($this->menu->getSelectableStyle());
128
129
        $this->addMenuItem($item);
130
131
        return $this;
132
    }
133
134
    public function addItems(array $items) : self
135
    {
136
        foreach ($items as $item) {
137
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to PhpSchool\CliMenu\Builde...iMenuBuilder::addItem() has too few arguments starting with itemCallable. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

137
            $this->/** @scrutinizer ignore-call */ 
138
                   addItem(...$item);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
138
        }
139
140
        return $this;
141
    }
142
143
    public function addCheckableItem(
144
        string $text,
145
        callable $itemCallable,
146
        bool $showItemExtra = false,
147
        bool $disabled = false
148
    ) : self {
149
        $item = (new CheckableItem($text, $itemCallable, $showItemExtra, $disabled))
150
            ->setStyle($this->menu->getCheckableStyle());
151
152
        $this->addMenuItem($item);
153
154
        return $this;
155
    }
156
157
    public function addRadioItem(
158
        string $text,
159
        callable $itemCallable,
160
        bool $showItemExtra = false,
161
        bool $disabled = false
162
    ) : self {
163
        $item = (new RadioItem($text, $itemCallable, $showItemExtra, $disabled))
164
            ->setStyle($this->menu->getRadioStyle());
165
166
        $this->addMenuItem($item);
167
168
        return $this;
169
    }
170
171
    public function addStaticItem(string $text) : self
172
    {
173
        $this->addMenuItem(new StaticItem($text));
174
175
        return $this;
176
    }
177
178
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
179
    {
180
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
181
182
        return $this;
183
    }
184
185
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
186
    {
187
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
188
189
        return $this;
190
    }
191
192
    public function addSubMenu(string $text, Closure $callback) : self
193
    {
194
        $builder = self::newSubMenu($this->terminal);
195
196
        if ($this->autoShortcuts) {
197
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
198
        }
199
200
        $callback($builder);
201
202
        $menu = $this->createMenuClosure($builder);
203
204
        $item = (new MenuMenuItem($text, $menu, $builder->isMenuDisabled()))
205
            ->setStyle($this->menu->getSelectableStyle());
206
207
        $this->menu->addItem($item);
208
209
        $this->processItemShortcut($item);
210
211
        return $this;
212
    }
213
214
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
215
    {
216
        $menu = $this->createMenuClosure($builder);
217
218
        $item = (new MenuMenuItem($text, $menu, $builder->isMenuDisabled()))
219
            ->setStyle($this->menu->getSelectableStyle());
220
221
        $this->menu->addItem($item);
222
223
        $this->processItemShortcut($item);
224
225
        return $this;
226
    }
227
228
    /**
229
     * Create the submenu as a closure which is then unpacked in MenuMenuItem::showSubMenu
230
     * This allows us to wait until all user-provided styles are parsed and apply them to nested items
231
     *
232
     * @param CliMenuBuilder|SplitItemBuilder $builder
233
     * @return Closure
234
     */
235
    protected function createMenuClosure($builder) : Closure
236
    {
237
        return function () use ($builder) {
238
            $menu = $builder->build();
239
240
            $menu->setParent($this->menu);
0 ignored issues
show
Bug introduced by
The method setParent() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

240
            $menu->/** @scrutinizer ignore-call */ 
241
                   setParent($this->menu);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
241
242
            // we apply the parent theme if nothing was changed
243
            // if no styles were changed in this sub-menu
244
            if (!$menu->getStyle()->hasChangedFromDefaults()) {
0 ignored issues
show
Bug introduced by
The method getStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

244
            if (!$menu->/** @scrutinizer ignore-call */ getStyle()->hasChangedFromDefaults()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
245
                $menu->setStyle($this->menu->getStyle());
0 ignored issues
show
Bug introduced by
The method setStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

245
                $menu->/** @scrutinizer ignore-call */ 
246
                       setStyle($this->menu->getStyle());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
246
            }
247
248
            // If user changed this style, persist to the menu so children CheckableItems may use it
249
            if ($this->menu->getCheckableStyle()->getIsCustom()) {
250
                $menu->setCheckableStyle(function (CheckableStyle $style) {
0 ignored issues
show
Bug introduced by
The method setCheckableStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

250
                $menu->/** @scrutinizer ignore-call */ 
251
                       setCheckableStyle(function (CheckableStyle $style) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
251
                    $style->fromArray($this->menu->getCheckableStyle()->toArray());
252
                });
253
            }
254
255
            // If user changed this style, persist to the menu so children RadioItems may use it
256
            if ($this->menu->getRadioStyle()->getIsCustom()) {
257
                $menu->setRadioStyle(function (RadioStyle $style) {
0 ignored issues
show
Bug introduced by
The method setRadioStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

257
                $menu->/** @scrutinizer ignore-call */ 
258
                       setRadioStyle(function (RadioStyle $style) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
258
                    $style->fromArray($this->menu->getRadioStyle()->toArray());
259
                });
260
            }
261
262
            // If user changed this style, persist to the menu so children SelectableItems may use it
263
            if ($this->menu->getSelectableStyle()->getIsCustom()) {
264
                $menu->setSelectableStyle(function (SelectableStyle $style) {
0 ignored issues
show
Bug introduced by
The method setSelectableStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

264
                $menu->/** @scrutinizer ignore-call */ 
265
                       setSelectableStyle(function (SelectableStyle $style) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
265
                    $style->fromArray($this->menu->getSelectableStyle()->toArray());
266
                });
267
            }
268
269
            // This will be filled with user-provided items
270
            foreach ($menu->getItems() as $item) {
271
                if ($item instanceof SelectableInterface && !$item->getStyle()->getIsCustom()) {
272
                    $item->setStyle(clone $menu->getSelectableStyle());
0 ignored issues
show
Bug introduced by
The method getSelectableStyle() does not exist on PhpSchool\CliMenu\MenuItem\SplitItem. Did you maybe mean getSelectedItem()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
                    $item->setStyle(clone $menu->/** @scrutinizer ignore-call */ getSelectableStyle());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
273
                }
274
            }
275
276
            return $menu;
277
        };
278
    }
279
280
    public function enableAutoShortcuts(string $regex = null) : self
281
    {
282
        $this->autoShortcuts = true;
283
284
        if (null !== $regex) {
285
            $this->autoShortcutsRegex = $regex;
286
        }
287
288
        return $this;
289
    }
290
291
    private function extractShortcut(string $title) : ?string
292
    {
293
        preg_match($this->autoShortcutsRegex, $title, $match);
294
295
        if (!isset($match[1])) {
296
            return null;
297
        }
298
299
        if (mb_strlen($match[1]) > 1) {
300
            throw InvalidShortcutException::fromShortcut($match[1]);
301
        }
302
303
        return isset($match[1]) ? strtolower($match[1]) : null;
304
    }
305
306
    private function processItemShortcut(MenuItemInterface $item) : void
307
    {
308
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
309
            $menu->executeAsSelected($item);
310
        });
311
    }
312
313
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
314
    {
315
        foreach ($splitItem->getItems() as $item) {
316
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
317
                $current = $splitItem->getSelectedItemIndex();
318
319
                $splitItem->setSelectedItemIndex(
320
                    array_search($item, $splitItem->getItems(), true)
0 ignored issues
show
Bug introduced by
It seems like array_search($item, $splitItem->getItems(), true) can also be of type false and string; however, parameter $index of PhpSchool\CliMenu\MenuIt...:setSelectedItemIndex() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

320
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
321
                );
322
323
                $menu->executeAsSelected($splitItem);
324
325
                if ($current !== null) {
326
                    $splitItem->setSelectedItemIndex($current);
327
                }
328
            });
329
        }
330
    }
331
332
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
333
    {
334
        if (!$this->autoShortcuts) {
335
            return;
336
        }
337
338
        if ($shortcut = $this->extractShortcut($item->getText())) {
339
            $this->menu->addCustomControlMapping(
340
                $shortcut,
341
                $callback
342
            );
343
        }
344
    }
345
346
    public function addSplitItem(Closure $callback) : self
347
    {
348
        $builder = new SplitItemBuilder($this->menu);
349
350
        if ($this->autoShortcuts) {
351
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
352
        }
353
354
        // If user changed this style, persist to the menu so children CheckableItems may use it
355
        if ($this->menu->getCheckableStyle()->getIsCustom()) {
356
            $builder->setCheckableStyle(function (CheckableStyle $style) {
357
                $style->fromArray($this->menu->getCheckableStyle()->toArray());
358
            });
359
        }
360
361
        // If user changed this style, persist to the menu so children RadioItems may use it
362
        if ($this->menu->getRadioStyle()->getIsCustom()) {
363
            $builder->setRadioStyle(function (RadioStyle $style) {
364
                $style->fromArray($this->menu->getRadioStyle()->toArray());
365
            });
366
        }
367
368
        // If user changed this style, persist to the menu so children SelectableItems may use it
369
        if ($this->menu->getSelectableStyle()->getIsCustom()) {
370
            $builder->setSelectableStyle(function (SelectableStyle $style) {
371
                $style->fromArray($this->menu->getSelectableStyle()->toArray());
372
            });
373
        }
374
375
        $callback($builder);
376
        
377
        $this->menu->addItem($splitItem = $builder->build());
378
379
        $this->processSplitItemShortcuts($splitItem);
380
381
        return $this;
382
    }
383
384
    /**
385
     * Disable a submenu
386
     *
387
     * @throws \InvalidArgumentException
388
     */
389
    public function disableMenu() : self
390
    {
391
        if (!$this->subMenu) {
392
            throw new \InvalidArgumentException(
393
                'You can\'t disable the root menu'
394
            );
395
        }
396
397
        $this->disabled = true;
398
399
        return $this;
400
    }
401
402
    public function isMenuDisabled() : bool
403
    {
404
        return $this->disabled;
405
    }
406
407
    public function setGoBackButtonText(string $goBackButtonTest) : self
408
    {
409
        $this->goBackButtonText = $goBackButtonTest;
410
411
        return $this;
412
    }
413
414
    public function setExitButtonText(string $exitButtonText) : self
415
    {
416
        $this->exitButtonText = $exitButtonText;
417
418
        return $this;
419
    }
420
421
    public function setBackgroundColour(string $colour, string $fallback = null) : self
422
    {
423
        $this->style->setBg($colour, $fallback);
424
425
        return $this;
426
    }
427
428
    public function setForegroundColour(string $colour, string $fallback = null) : self
429
    {
430
        $this->style->setFg($colour, $fallback);
431
432
        return $this;
433
    }
434
435
    public function setWidth(int $width) : self
436
    {
437
        $this->style->setWidth($width);
438
439
        return $this;
440
    }
441
442
    public function setPadding(int $topBottom, int $leftRight = null) : self
443
    {
444
        $this->style->setPadding($topBottom, $leftRight);
445
446
        return $this;
447
    }
448
449
    public function setPaddingTopBottom(int $topBottom) : self
450
    {
451
        $this->style->setPaddingTopBottom($topBottom);
452
453
        return $this;
454
    }
455
456
    public function setPaddingLeftRight(int $leftRight) : self
457
    {
458
        $this->style->setPaddingLeftRight($leftRight);
459
460
        return $this;
461
    }
462
463
    public function setMarginAuto() : self
464
    {
465
        $this->style->setMarginAuto();
466
467
        return $this;
468
    }
469
470
    public function setMargin(int $margin) : self
471
    {
472
        $this->style->setMargin($margin);
473
474
        return $this;
475
    }
476
477
    public function setUnselectedMarker(string $marker) : self
478
    {
479
        $this->style->setUnselectedMarker($marker);
480
481
        return $this;
482
    }
483
484
    public function setSelectedMarker(string $marker) : self
485
    {
486
        $this->style->setSelectedMarker($marker);
487
488
        return $this;
489
    }
490
491
    public function setItemExtra(string $extra) : self
492
    {
493
        $this->style->setItemExtra($extra);
494
495
        //if we customise item extra, it means we most likely want to display it
496
        $this->displayExtra();
497
498
        return $this;
499
    }
500
501
    public function setTitleSeparator(string $separator) : self
502
    {
503
        $this->style->setTitleSeparator($separator);
504
505
        return $this;
506
    }
507
508
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
509
    {
510
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
511
512
        return $this;
513
    }
514
515
    public function setBorderTopWidth(int $width) : self
516
    {
517
        $this->style->setBorderTopWidth($width);
518
        
519
        return $this;
520
    }
521
522
    public function setBorderRightWidth(int $width) : self
523
    {
524
        $this->style->setBorderRightWidth($width);
525
526
        return $this;
527
    }
528
529
    public function setBorderBottomWidth(int $width) : self
530
    {
531
        $this->style->setBorderBottomWidth($width);
532
533
        return $this;
534
    }
535
536
    public function setBorderLeftWidth(int $width) : self
537
    {
538
        $this->style->setBorderLeftWidth($width);
539
540
        return $this;
541
    }
542
543
    public function setBorderColour(string $colour, $fallback = null) : self
544
    {
545
        $this->style->setBorderColour($colour, $fallback);
546
547
        return $this;
548
    }
549
550
    public function getStyle() : MenuStyle
551
    {
552
        return $this->style;
553
    }
554
555
    public function getTerminal() : Terminal
556
    {
557
        return $this->terminal;
558
    }
559
560
    private function getDefaultItems() : array
561
    {
562
        $actions = [];
563
        if ($this->subMenu) {
564
            $actions[] = (new SelectableItem($this->goBackButtonText, new GoBackAction))
565
                ->setStyle($this->menu->getSelectableStyle());
566
        }
567
568
        $actions[] = (new SelectableItem($this->exitButtonText, new ExitAction))
569
                ->setStyle($this->menu->getSelectableStyle());
570
571
        return $actions;
572
    }
573
574
    public function disableDefaultItems() : self
575
    {
576
        $this->disableDefaultItems = true;
577
578
        return $this;
579
    }
580
581
    public function displayExtra() : self
582
    {
583
        $this->style->setDisplaysExtra(true);
584
585
        return $this;
586
    }
587
588
    private function itemsHaveExtra(array $items) : bool
589
    {
590
        return !empty(array_filter($items, function (MenuItemInterface $item) {
591
            return $item->showsItemExtra();
592
        }));
593
    }
594
    
595
    public function build() : CliMenu
596
    {
597
        if (!$this->disableDefaultItems) {
598
            $this->menu->addItems($this->getDefaultItems());
599
        }
600
601
        if (!$this->style->getDisplaysExtra()) {
602
            $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
603
        }
604
605
        return $this->menu;
606
    }
607
608
    public function setCheckableStyle(callable $itemCallable) : self
609
    {
610
        $this->menu->setCheckableStyle($itemCallable);
611
612
        return $this;
613
    }
614
615
    public function setRadioStyle(callable $itemCallable) : self
616
    {
617
        $this->menu->setRadioStyle($itemCallable);
618
619
        return $this;
620
    }
621
622
    public function setSelectableStyle(callable $itemCallable) : self
623
    {
624
        $this->menu->setSelectableStyle($itemCallable);
625
626
        return $this;
627
    }
628
}
629