Completed
Push — master ( 3c4db9...42aad0 )
by Aydin
01:00 queued 21s
created

CliMenuBuilder::addMenuItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

259
                    /** @scrutinizer ignore-type */ array_search($item, $splitItem->getItems(), true)
Loading history...
260
                );
261
262
                $menu->executeAsSelected($splitItem);
263
264
                if ($current !== null) {
265
                    $splitItem->setSelectedItemIndex($current);
266
                }
267
            });
268
        }
269
    }
270
271
    private function processIndividualShortcut(MenuItemInterface $item, callable $callback) : void
272
    {
273
        if (!$this->autoShortcuts) {
274
            return;
275
        }
276
277
        if ($shortcut = $this->extractShortcut($item->getText())) {
278
            $this->menu->addCustomControlMapping(
279
                $shortcut,
280
                $callback
281
            );
282
        }
283
    }
284
285
    public function addSplitItem(\Closure $callback) : self
286
    {
287
        $builder = new SplitItemBuilder($this->menu);
288
289
        if ($this->autoShortcuts) {
290
            $builder->enableAutoShortcuts($this->autoShortcutsRegex);
291
        }
292
293
        $callback = $callback->bindTo($builder);
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 setUncheckedMarker(string $marker) : self
411
    {
412
        $this->style->setUncheckedMarker($marker);
413
414
        return $this;
415
    }
416
417
    public function setCheckedMarker(string $marker) : self
418
    {
419
        $this->style->setCheckedMarker($marker);
420
421
        return $this;
422
    }
423
424
    public function setItemExtra(string $extra) : self
425
    {
426
        $this->style->setItemExtra($extra);
427
428
        //if we customise item extra, it means we most likely want to display it
429
        $this->displayExtra();
430
431
        return $this;
432
    }
433
434
    public function setTitleSeparator(string $separator) : self
435
    {
436
        $this->style->setTitleSeparator($separator);
437
438
        return $this;
439
    }
440
441
    public function setBorder(int $top, $right = null, $bottom = null, $left = null, string $colour = null) : self
442
    {
443
        $this->style->setBorder($top, $right, $bottom, $left, $colour);
444
445
        return $this;
446
    }
447
448
    public function setBorderTopWidth(int $width) : self
449
    {
450
        $this->style->setBorderTopWidth($width);
451
        
452
        return $this;
453
    }
454
455
    public function setBorderRightWidth(int $width) : self
456
    {
457
        $this->style->setBorderRightWidth($width);
458
459
        return $this;
460
    }
461
462
    public function setBorderBottomWidth(int $width) : self
463
    {
464
        $this->style->setBorderBottomWidth($width);
465
466
        return $this;
467
    }
468
469
    public function setBorderLeftWidth(int $width) : self
470
    {
471
        $this->style->setBorderLeftWidth($width);
472
473
        return $this;
474
    }
475
476
    public function setBorderColour(string $colour, $fallback = null) : self
477
    {
478
        $this->style->setBorderColour($colour, $fallback);
479
480
        return $this;
481
    }
482
483
    public function getStyle() : MenuStyle
484
    {
485
        return $this->style;
486
    }
487
488
    public function getTerminal() : Terminal
489
    {
490
        return $this->terminal;
491
    }
492
493
    private function getDefaultItems() : array
494
    {
495
        $actions = [];
496
        if ($this->subMenu) {
497
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
498
        }
499
500
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
501
        return $actions;
502
    }
503
504
    public function disableDefaultItems() : self
505
    {
506
        $this->disableDefaultItems = true;
507
508
        return $this;
509
    }
510
511
    public function displayExtra() : self
512
    {
513
        $this->style->setDisplaysExtra(true);
514
515
        return $this;
516
    }
517
518
    private function itemsHaveExtra(array $items) : bool
519
    {
520
        return !empty(array_filter($items, function (MenuItemInterface $item) {
521
            return $item->showsItemExtra();
522
        }));
523
    }
524
    
525
    public function build() : CliMenu
526
    {
527
        if (!$this->disableDefaultItems) {
528
            $this->menu->addItems($this->getDefaultItems());
529
        }
530
531
        if (!$this->style->getDisplaysExtra()) {
532
            $this->style->setDisplaysExtra($this->itemsHaveExtra($this->menu->getItems()));
533
        }
534
535
        return $this->menu;
536
    }
537
}
538