Completed
Push — master ( da5a0b...090289 )
by Aydin
18s queued 11s
created

CliMenuBuilder::propagateStyles()   C

Complexity

Conditions 12
Paths 146

Size

Total Lines 39
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 19
c 0
b 0
f 0
nc 146
nop 2
dl 0
loc 39
rs 6.5833

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        $this->addMenuItem(new CheckboxItem($text, $itemCallable, $showItemExtra, $disabled));
144
145
        return $this;
146
    }
147
148
    public function addRadioItem(
149
        string $text,
150
        callable $itemCallable,
151
        bool $showItemExtra = false,
152
        bool $disabled = false
153
    ) : self {
154
        $this->addMenuItem(new RadioItem($text, $itemCallable, $showItemExtra, $disabled));
155
156
        return $this;
157
    }
158
159
    public function addStaticItem(string $text) : self
160
    {
161
        $this->addMenuItem(new StaticItem($text));
162
163
        return $this;
164
    }
165
166
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
167
    {
168
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
169
170
        return $this;
171
    }
172
173
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
174
    {
175
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
176
177
        return $this;
178
    }
179
180
    public function addSubMenu(string $text, \Closure $callback) : self
181
    {
182
        $builder = self::newSubMenu($this->terminal);
183
184
        if ($this->autoShortcuts) {
185
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
186
        }
187
188
        $callback($builder);
189
190
        $menu = $builder->build();
191
        $menu->setParent($this->menu);
192
193
        $this->menu->addItem($item = new MenuMenuItem(
194
            $text,
195
            $menu,
196
            $builder->isMenuDisabled()
197
        ));
198
199
        $this->processItemShortcut($item);
200
201
        return $this;
202
    }
203
204
    public function addSubMenuFromBuilder(string $text, CliMenuBuilder $builder) : self
205
    {
206
        $menu = $builder->build();
207
        $menu->setParent($this->menu);
208
209
        $this->menu->addItem($item = new MenuMenuItem(
210
            $text,
211
            $menu,
212
            $builder->isMenuDisabled()
213
        ));
214
215
        $this->processItemShortcut($item);
216
217
        return $this;
218
    }
219
220
    public function enableAutoShortcuts(string $regex = null) : self
221
    {
222
        $this->autoShortcuts = true;
223
224
        if (null !== $regex) {
225
            $this->autoShortcutsRegex = $regex;
226
        }
227
228
        return $this;
229
    }
230
231
    private function extractShortcut(string $title) : ?string
232
    {
233
        preg_match($this->autoShortcutsRegex, $title, $match);
234
235
        if (!isset($match[1])) {
236
            return null;
237
        }
238
239
        if (mb_strlen($match[1]) > 1) {
240
            throw InvalidShortcutException::fromShortcut($match[1]);
241
        }
242
243
        return isset($match[1]) ? strtolower($match[1]) : null;
244
    }
245
246
    private function processItemShortcut(MenuItemInterface $item) : void
247
    {
248
        $this->processIndividualShortcut($item, function (CliMenu $menu) use ($item) {
249
            $menu->executeAsSelected($item);
250
        });
251
    }
252
253
    private function processSplitItemShortcuts(SplitItem $splitItem) : void
254
    {
255
        foreach ($splitItem->getItems() as $item) {
256
            $this->processIndividualShortcut($item, function (CliMenu $menu) use ($splitItem, $item) {
257
                $current = $splitItem->getSelectedItemIndex();
258
259
                $splitItem->setSelectedItemIndex(
260
                    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

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