CliMenuBuilder   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 153
c 4
b 0
f 0
dl 0
loc 474
rs 3.52
wmc 61

45 Methods

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

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

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

247
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
248
                );
249
250
                $menu->executeAsSelected($splitItem);
251
252
                if ($current !== null) {
253
                    $splitItem->setSelectedItemIndex($current);
254
                }
255
            });
256
        }
257
    }
258
259
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
260
    {
261
        if (!$this->autoShortcuts) {
262
            return;
263
        }
264
265
        if ($shortcut = $this->extractShortcut($item->getText())) {
266
            $this->menu->addCustomControlMapping(
267
                $shortcut,
268
                $callback
269
            );
270
        }
271
    }
272
273
    public function addSplitItem(\Closure $callback) : self
274
    {
275
        $builder = new SplitItemBuilder($this->menu);
276
277
        if ($this->autoShortcuts) {
278
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
279
        }
280
281
        $callback = $callback->bindTo($builder);
282
        $callback($builder);
283
        
284
        $this->menu->addItem($splitItem = $builder->build());
285
286
        $this->processSplitItemShortcuts($splitItem);
287
288
        return $this;
289
    }
290
291
    /**
292
     * Disable a submenu
293
     *
294
     * @throws \InvalidArgumentException
295
     */
296
    public function disableMenu() : self
297
    {
298
        if (!$this->subMenu) {
299
            throw new \InvalidArgumentException(
300
                'You can\'t disable the root menu'
301
            );
302
        }
303
304
        $this->disabled = true;
305
306
        return $this;
307
    }
308
309
    public function isMenuDisabled() : bool
310
    {
311
        return $this->disabled;
312
    }
313
314
    public function setGoBackButtonText(string $goBackButtonTest) : self
315
    {
316
        $this->goBackButtonText = $goBackButtonTest;
317
318
        return $this;
319
    }
320
321
    public function setExitButtonText(string $exitButtonText) : self
322
    {
323
        $this->exitButtonText = $exitButtonText;
324
325
        return $this;
326
    }
327
328
    public function setBackgroundColour(string $colour, string $fallback = null) : self
329
    {
330
        $this->style->setBg($colour, $fallback);
331
332
        return $this;
333
    }
334
335
    public function setForegroundColour(string $colour, string $fallback = null) : self
336
    {
337
        $this->style->setFg($colour, $fallback);
338
339
        return $this;
340
    }
341
342
    public function setWidth(int $width) : self
343
    {
344
        $this->style->setWidth($width);
345
346
        return $this;
347
    }
348
349
    public function setPadding(int $topBottom, int $leftRight = null) : self
350
    {
351
        $this->style->setPadding($topBottom, $leftRight);
352
353
        return $this;
354
    }
355
356
    public function setPaddingTopBottom(int $topBottom) : self
357
    {
358
        $this->style->setPaddingTopBottom($topBottom);
359
360
        return $this;
361
    }
362
363
    public function setPaddingLeftRight(int $leftRight) : self
364
    {
365
        $this->style->setPaddingLeftRight($leftRight);
366
367
        return $this;
368
    }
369
370
    public function setMarginAuto() : self
371
    {
372
        $this->style->setMarginAuto();
373
374
        return $this;
375
    }
376
377
    public function setMargin(int $margin) : self
378
    {
379
        $this->style->setMargin($margin);
380
381
        return $this;
382
    }
383
384
    public function setUnselectedMarker(string $marker) : self
385
    {
386
        $this->style->setUnselectedMarker($marker);
387
388
        return $this;
389
    }
390
391
    public function setSelectedMarker(string $marker) : self
392
    {
393
        $this->style->setSelectedMarker($marker);
394
395
        return $this;
396
    }
397
398
    public function setItemExtra(string $extra) : self
399
    {
400
        $this->style->setItemExtra($extra);
401
402
        return $this;
403
    }
404
405
    public function setTitleSeparator(string $separator) : self
406
    {
407
        $this->style->setTitleSeparator($separator);
408
409
        return $this;
410
    }
411
412
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
413
    {
414
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
415
416
        return $this;
417
    }
418
419
    public function setBorderTopWidth(int $width) : self
420
    {
421
        $this->style->setBorderTopWidth($width);
422
        
423
        return $this;
424
    }
425
426
    public function setBorderRightWidth(int $width) : self
427
    {
428
        $this->style->setBorderRightWidth($width);
429
430
        return $this;
431
    }
432
433
    public function setBorderBottomWidth(int $width) : self
434
    {
435
        $this->style->setBorderBottomWidth($width);
436
437
        return $this;
438
    }
439
440
    public function setBorderLeftWidth(int $width) : self
441
    {
442
        $this->style->setBorderLeftWidth($width);
443
444
        return $this;
445
    }
446
447
    public function setBorderColour(string $colour, $fallback = null) : self
448
    {
449
        $this->style->setBorderColour($colour, $fallback);
450
451
        return $this;
452
    }
453
454
    public function getStyle() : MenuStyle
455
    {
456
        return $this->style;
457
    }
458
459
    public function getTerminal() : Terminal
460
    {
461
        return $this->terminal;
462
    }
463
464
    private function getDefaultItems() : array
465
    {
466
        $actions = [];
467
        if ($this->subMenu) {
468
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
469
        }
470
471
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
472
        return $actions;
473
    }
474
475
    public function disableDefaultItems() : self
476
    {
477
        $this->disableDefaultItems = true;
478
479
        return $this;
480
    }
481
482
    private function itemsHaveExtra(array $items) : bool
483
    {
484
        return !empty(array_filter($items, function (MenuItemInterface $item) {
485
            return $item->showsItemExtra();
486
        }));
487
    }
488
    
489
    public function build() : CliMenu
490
    {
491
        if (!$this->disableDefaultItems) {
492
            $this->menu->addItems($this->getDefaultItems());
493
        }
494
495
        $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
496
497
        return $this->menu;
498
    }
499
}
500