Completed
Push — master ( 867de8...1b7820 )
by Aydin
20s queued 12s
created

CliMenuBuilder   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 169
dl 0
loc 536
rs 2.88
c 5
b 0
f 0
wmc 69

52 Methods

Rating   Name   Duplication   Size   Complexity  
A setTitleSeparator() 0 5 1
A setWidth() 0 5 1
A processItemShortcut() 0 4 1
A newSubMenu() 0 6 1
A setTitle() 0 5 1
A setUnselectedMarker() 0 5 1
A setBorderRightWidth() 0 5 1
A setItemExtra() 0 8 1
A setBackgroundColour() 0 5 1
A setSelectedMarker() 0 5 1
A setForegroundColour() 0 5 1
A setMargin() 0 5 1
A setBorderColour() 0 5 1
A enableAutoShortcuts() 0 9 2
A setPadding() 0 5 1
A disableDefaultItems() 0 5 1
A extractShortcut() 0 13 4
A addMenuItem() 0 7 1
A addAsciiArt() 0 5 1
A setBorder() 0 5 1
A disableMenu() 0 11 2
A processIndividualShortcut() 0 10 3
A addSubMenuFromBuilder() 0 20 2
A addStaticItem() 0 5 1
A addCheckableItem() 0 9 1
A setBorderLeftWidth() 0 5 1
A setRadioMarker() 0 5 1
A setMarginAuto() 0 5 1
A setPaddingTopBottom() 0 5 1
A displayExtra() 0 5 1
A __construct() 0 5 1
A getTerminal() 0 3 1
A setBorderBottomWidth() 0 5 1
A setPaddingLeftRight() 0 5 1
A setGoBackButtonText() 0 5 1
A setUnradioMarker() 0 5 1
A getStyle() 0 3 1
A addItem() 0 9 1
A build() 0 11 3
A addSplitItem() 0 16 2
A addLineBreak() 0 5 1
A getDefaultItems() 0 9 2
A processSplitItemShortcuts() 0 14 3
A addSubMenu() 0 29 3
A addRadioItem() 0 9 1
A setBorderTopWidth() 0 5 1
A isMenuDisabled() 0 3 1
A setUncheckedMarker() 0 5 1
A addItems() 0 7 2
A itemsHaveExtra() 0 4 1
A setCheckedMarker() 0 5 1
A setExitButtonText() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like CliMenuBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CliMenuBuilder, and based on these observations, apply Extract Interface, too.

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\CheckableItem;
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\Terminal\TerminalFactory;
20
use PhpSchool\Terminal\Terminal;
21
22
/**
23
 * @author Michael Woodward <[email protected]>
24
 * @author Aydin Hassan <[email protected]>
25
 */
26
class CliMenuBuilder
27
{
28
    /**
29
     * @var CliMenu
30
     */
31
    private $menu;
32
33
    /**
34
     * @var string
35
     */
36
    private $goBackButtonText = 'Go Back';
37
38
    /**
39
     * @var string
40
     */
41
    private $exitButtonText = 'Exit';
42
43
    /**
44
     * @var MenuStyle
45
     */
46
    private $style;
47
48
    /**
49
     * @var Terminal
50
     */
51
    private $terminal;
52
53
    /**
54
     * @var bool
55
     */
56
    private $disableDefaultItems = false;
57
58
    /**
59
     * @var bool
60
     */
61
    private $disabled = false;
62
63
    /**
64
     * Whether or not to auto create keyboard shortcuts for items
65
     * when they contain square brackets. Eg: [M]y item
66
     *
67
     * @var bool
68
     */
69
    private $autoShortcuts = false;
70
71
    /**
72
     * Regex to auto match for shortcuts defaults to looking
73
     * for a single character encased in square brackets
74
     *
75
     * @var string
76
     */
77
    private $autoShortcutsRegex = '/\[(.)\]/';
78
79
    /**
80
     * @var bool
81
     */
82
    private $subMenu = false;
83
84
    public function __construct(Terminal $terminal = null)
85
    {
86
        $this->terminal = $terminal ?? TerminalFactory::fromSystem();
87
        $this->style    = new MenuStyle($this->terminal);
88
        $this->menu     = new CliMenu(null, [], $this->terminal, $this->style);
89
    }
90
    
91
    public static function newSubMenu(Terminal $terminal) : self
92
    {
93
        $instance = new self($terminal);
94
        $instance->subMenu = true;
95
        
96
        return $instance;
97
    }
98
99
    public function setTitle(string $title) : self
100
    {
101
        $this->menu->setTitle($title);
102
103
        return $this;
104
    }
105
106
    public function addMenuItem(MenuItemInterface $item) : self
107
    {
108
        $this->menu->addItem($item);
109
110
        $this->processItemShortcut($item);
111
112
        return $this;
113
    }
114
115
    public function addItem(
116
        string $text,
117
        callable $itemCallable,
118
        bool $showItemExtra = false,
119
        bool $disabled = false
120
    ) : self {
121
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
122
123
        return $this;
124
    }
125
126
    public function addItems(array $items) : self
127
    {
128
        foreach ($items as $item) {
129
            $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

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

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