Passed
Pull Request — master (#203)
by
unknown
02:34
created

CliMenuBuilder::setMarginAuto()   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 0
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, $this->menu);
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, $this->menu);
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
     * @param CliMenu $parent
234
     * @return Closure
235
     */
236
    protected function createMenuClosure($builder, CliMenu $parent = null) : Closure
237
    {
238
        return function () use ($builder, $parent) {
239
            $menu = $builder->build();
240
241
            if ($parent) {
242
                $menu->setParent($parent);
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

242
                $menu->/** @scrutinizer ignore-call */ 
243
                       setParent($parent);

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...
243
            }
244
245
            // we apply the parent theme if nothing was changed
246
            // if no styles were changed in this sub-menu
247
            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

247
            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...
248
                $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

248
                $menu->/** @scrutinizer ignore-call */ 
249
                       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...
249
            }
250
251
            // If user changed this style, persist to the menu so children CheckableItems may use it
252
            if ($this->menu->getCheckableStyle()->getIsCustom()) {
253
                $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

253
                $menu->/** @scrutinizer ignore-call */ 
254
                       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...
254
                    $style->fromArray($this->menu->getCheckableStyle()->toArray());
255
                });
256
            }
257
258
            // If user changed this style, persist to the menu so children RadioItems may use it
259
            if ($this->menu->getRadioStyle()->getIsCustom()) {
260
                $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

260
                $menu->/** @scrutinizer ignore-call */ 
261
                       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...
261
                    $style->fromArray($this->menu->getRadioStyle()->toArray());
262
                });
263
            }
264
265
            // If user changed this style, persist to the menu so children SelectableItems may use it
266
            if ($this->menu->getSelectableStyle()->getIsCustom()) {
267
                $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

267
                $menu->/** @scrutinizer ignore-call */ 
268
                       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...
268
                    $style->fromArray($this->menu->getSelectableStyle()->toArray());
269
                });
270
            }
271
272
            // This will be filled with user-provided items
273
            foreach ($menu->getItems() as $item) {
274
                if ($item instanceof SelectableInterface && !$item->getStyle()->getIsCustom()) {
275
                    $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

275
                    $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...
276
                }
277
            }
278
279
            return $menu;
280
        };
281
    }
282
283
    public function enableAutoShortcuts(string $regex = null) : self
284
    {
285
        $this->autoShortcuts = true;
286
287
        if (null !== $regex) {
288
            $this->autoShortcutsRegex = $regex;
289
        }
290
291
        return $this;
292
    }
293
294
    private function extractShortcut(string $title) : ?string
295
    {
296
        preg_match($this->autoShortcutsRegex, $title, $match);
297
298
        if (!isset($match[1])) {
299
            return null;
300
        }
301
302
        if (mb_strlen($match[1]) > 1) {
303
            throw InvalidShortcutException::fromShortcut($match[1]);
304
        }
305
306
        return isset($match[1]) ? strtolower($match[1]) : null;
307
    }
308
309
    private function processItemShortcut(MenuItemInterface $item) : void
310
    {
311
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
312
            $menu->executeAsSelected($item);
313
        });
314
    }
315
316
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
317
    {
318
        foreach ($splitItem->getItems() as $item) {
319
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
320
                $current = $splitItem->getSelectedItemIndex();
321
322
                $splitItem->setSelectedItemIndex(
323
                    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

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