Passed
Pull Request — master (#205)
by Aydin
02:02
created

CliMenuBuilder::addSubMenu()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

131
            $this->/** @scrutinizer ignore-call */ 
132
                   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...
132
        }
133
134
        return $this;
135
    }
136
137
    public function addCheckboxItem(
138
        string $text,
139
        callable $itemCallable,
140
        bool $showItemExtra = false,
141
        bool $disabled = false
142
    ) : self {
143
        $item = (new CheckboxItem($text, $itemCallable, $showItemExtra, $disabled))
144
            ->setStyle($this->menu->getCheckboxStyle());
145
146
        $this->addMenuItem($item);
147
148
        return $this;
149
    }
150
151
    public function addRadioItem(
152
        string $text,
153
        callable $itemCallable,
154
        bool $showItemExtra = false,
155
        bool $disabled = false
156
    ) : self {
157
        $item = (new RadioItem($text, $itemCallable, $showItemExtra, $disabled))
158
            ->setStyle($this->menu->getRadioStyle());
159
160
        $this->addMenuItem($item);
161
162
        return $this;
163
    }
164
165
    public function addStaticItem(string $text) : self
166
    {
167
        $this->addMenuItem(new StaticItem($text));
168
169
        return $this;
170
    }
171
172
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
173
    {
174
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
175
176
        return $this;
177
    }
178
179
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
180
    {
181
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
182
183
        return $this;
184
    }
185
186
    public function addSubMenu(string $text, \Closure $callback) : self
187
    {
188
        $builder = self::newSubMenu($this->terminal);
189
190
        if ($this->autoShortcuts) {
191
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
192
        }
193
194
        $callback($builder);
195
196
        $menu = $builder->build();
197
        $menu->setParent($this->menu);
198
        
199
        //we apply the parent theme if nothing was changed
200
        //if no styles were changed in this sub-menu
201
        if (!$menu->getStyle()->hasChangedFromDefaults()) {
202
            $menu->setStyle($this->menu->getStyle());
203
        }
204
205
        if (!$menu->getCheckboxStyle()->hasChangedFromDefaults()) {
206
            $menu->setCheckboxStyle(clone $this->menu->getCheckboxStyle());
207
        }
208
209
        if (!$menu->getRadioStyle()->hasChangedFromDefaults()) {
210
            $menu->setRadioStyle(clone $this->menu->getRadioStyle());
211
        }
212
213
        $this->menu->addItem($item = new MenuMenuItem(
214
            $text,
215
            $menu,
216
            $builder->isMenuDisabled()
217
        ));
218
219
        $this->processItemShortcut($item);
220
221
        return $this;
222
    }
223
224
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
225
    {
226
        $menu = $builder->build();
227
        $menu->setParent($this->menu);
228
229
        //we apply the parent theme if nothing was changed
230
        //if no styles were changed in this sub-menu
231
        if (!$menu->getStyle()->hasChangedFromDefaults()) {
232
            $menu->setStyle($this->menu->getStyle());
233
        }
234
235
        if (!$menu->getCheckboxStyle()->hasChangedFromDefaults()) {
236
            $menu->setCheckboxStyle(clone $this->menu->getCheckboxStyle());
237
        }
238
239
        if (!$menu->getRadioStyle()->hasChangedFromDefaults()) {
240
            $menu->setRadioStyle(clone $this->menu->getRadioStyle());
241
        }
242
243
        $this->menu->addItem($item = new MenuMenuItem(
244
            $text,
245
            $menu,
246
            $builder->isMenuDisabled()
247
        ));
248
249
        $this->processItemShortcut($item);
250
251
        return $this;
252
    }
253
254
    public function enableAutoShortcuts(string $regex = null) : self
255
    {
256
        $this->autoShortcuts = true;
257
258
        if (null !== $regex) {
259
            $this->autoShortcutsRegex = $regex;
260
        }
261
262
        return $this;
263
    }
264
265
    private function extractShortcut(string $title) : ?string
266
    {
267
        preg_match($this->autoShortcutsRegex, $title, $match);
268
269
        if (!isset($match[1])) {
270
            return null;
271
        }
272
273
        if (mb_strlen($match[1]) > 1) {
274
            throw InvalidShortcutException::fromShortcut($match[1]);
275
        }
276
277
        return isset($match[1]) ? strtolower($match[1]) : null;
278
    }
279
280
    private function processItemShortcut(MenuItemInterface $item) : void
281
    {
282
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
283
            $menu->executeAsSelected($item);
284
        });
285
    }
286
287
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
288
    {
289
        foreach ($splitItem->getItems() as $item) {
290
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
291
                $current = $splitItem->getSelectedItemIndex();
292
293
                $splitItem->setSelectedItemIndex(
294
                    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

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