Completed
Pull Request — master (#40)
by Michael
02:08
created

CliMenuBuilder::disableMenu()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 0
1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Action\ExitAction;
6
use PhpSchool\CliMenu\Action\GoBackAction;
7
use PhpSchool\CliMenu\MenuItem\AsciiArtItem;
8
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
9
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
10
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;
11
use PhpSchool\CliMenu\MenuItem\SelectableItem;
12
use PhpSchool\CliMenu\MenuItem\StaticItem;
13
use PhpSchool\CliMenu\Terminal\TerminalFactory;
14
use PhpSchool\CliMenu\Terminal\TerminalInterface;
15
use Assert\Assertion;
16
use RuntimeException;
17
18
/**
19
 * Class CliMenuBuilder
20
 *
21
 * @package PhpSchool\CliMenu
22
 * @author Michael Woodward <[email protected]>
23
 * @author Aydin Hassan <[email protected]>
24
 */
25
class CliMenuBuilder
26
{
27
    /**
28
     * @var bool
29
     */
30
    private $isBuilt = false;
31
32
    /**
33
     * @var null|self
34
     */
35
    private $parent;
36
    
37
    /**
38
     * @var self[]|CliMenu[]
39
     */
40
    private $subMenus = [];
41
42
    /**
43
     * @var string
44
     */
45
    private $goBackButtonText = 'Go Back';
46
    
47
    /**
48
     * @var string
49
     */
50
    private $exitButtonText = 'Exit';
51
52
    /**
53
     * @var array
54
     */
55
    private $menuItems = [];
56
57
    /**
58
     * @var array
59
     */
60
    private $style = [];
61
62
    /**
63
     * @var TerminalInterface
64
     */
65
    private $terminal;
66
67
    /**
68
     * @var string
69
     */
70
    private $menuTitle;
71
72
    /**
73
     * @var bool
74
     */
75
    private $disableDefaultItems = false;
76
77
    /**
78
     * @var bool
79
     */
80
    private $disabled;
81
82
    /**
83
     * @param CliMenuBuilder|null $parent
84
     */
85
    public function __construct(CliMenuBuilder $parent = null)
86
    {
87
        $this->parent            = $parent;
88
        $this->terminal          = TerminalFactory::fromSystem();
89
        $this->style             = $this->getStyleClassDefaults();
90
        $this->style['terminal'] = $this->terminal;
91
    }
92
93
    /**
94
     * Pull the constructor params into an array with default values
95
     *
96
     * @return array
97
     */
98
    private function getStyleClassDefaults()
99
    {
100
        $styleClassParameters = (new \ReflectionClass(MenuStyle::class))->getConstructor()->getParameters();
101
102
        $defaults = [];
103
        foreach ($styleClassParameters as $parameter) {
104
            $defaults[$parameter->getName()] = $parameter->getDefaultValue();
105
        }
106
107
        return $defaults;
108
    }
109
110
    /**
111
     * @param string $title
112
     * @return $this
113
     */
114
    public function setTitle($title)
115
    {
116
        Assertion::string($title);
117
118
        $this->menuTitle = $title;
119
120
        return $this;
121
    }
122
123
    /**
124
     * @param MenuItemInterface $item
125
     * @return $this
126
     */
127
    public function addMenuItem(MenuItemInterface $item)
128
    {
129
        $this->menuItems[] = $item;
130
131
        return $this;
132
    }
133
134
    /**
135
     * @param string $text
136
     * @param callable $itemCallable
137
     * @param bool $showItemExtra
138
     * @param bool $disabled
139
     * @return $this
140
     */
141
    public function addItem($text, callable $itemCallable, $showItemExtra = false, $disabled = false)
142
    {
143
        Assertion::string($text);
144
145
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
146
147
        return $this;
148
    }
149
150
    /**
151
     * @param array $items
152
     * @return $this
153
     */
154
    public function addItems(array $items)
155
    {
156
        foreach ($items as $item) {
157
            $this->addItem(...$item);
0 ignored issues
show
Bug introduced by
The call to addItem() misses a required argument $itemCallable.

This check looks for function calls that miss required arguments.

Loading history...
158
        }
159
        
160
        return $this;
161
    }
162
163
    /**
164
     * @param string $text
165
     * @return $this
166
     */
167
    public function addStaticItem($text)
168
    {
169
        Assertion::string($text);
170
171
        $this->addMenuItem(new StaticItem($text));
172
173
        return $this;
174
    }
175
176
    /**
177
     * @param string $breakChar
178
     * @param int $lines
179
     * @return $this
180
     */
181
    public function addLineBreak($breakChar = ' ', $lines = 1)
182
    {
183
        Assertion::string($breakChar);
184
        Assertion::integer($lines);
185
186
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
187
188
        return $this;
189
    }
190
191
    /**
192
     * @param string $art
193
     * @param string $position
194
     * @return $this
195
     */
196
    public function addAsciiArt($art, $position = AsciiArtItem::POSITION_CENTER)
197
    {
198
        Assertion::string($art);
199
        Assertion::string($position);
200
201
        $this->addMenuItem(new AsciiArtItem($art, $position));
202
203
        return $this;
204
    }
205
206
    /**
207
     * @param string $id ID to reference and retrieve sub-menu
208
     * @return CliMenuBuilder
209
     */
210
    public function addSubMenu($id)
211
    {
212
        Assertion::string($id);
213
214
        $this->menuItems[]   = $id;
215
        $this->subMenus[$id] = new self($this);
216
217
        return $this->subMenus[$id];
218
    }
219
220
    /**
221
     * Disable a submenu
222
     * @throws \InvalidArgumentException
223
     * @return $this
224
     */
225
    public function disableMenu()
226
    {
227
        if (!$this->parent) {
228
            throw new \InvalidArgumentException(
229
                'You can\'t disable the root menu'
230
            );
231
        }
232
233
        $this->disabled = true;
234
235
        return $this;
236
    }
237
238
    /**
239
     * @return bool
240
     */
241
    public function isMenuDisabled()
242
    {
243
        return $this->disabled;
244
    }
245
246
    /**
247
     * @param string $goBackButtonTest
248
     * @return $this
249
     */
250
    public function setGoBackButtonText($goBackButtonTest)
251
    {
252
        $this->goBackButtonText = $goBackButtonTest;
253
        
254
        return $this;
255
    }
256
257
    /**
258
     * @param string $exitButtonText
259
     * @return $this
260
     */
261
    public function setExitButtonText($exitButtonText)
262
    {
263
        $this->exitButtonText = $exitButtonText;
264
        
265
        return $this;
266
    }
267
268
    /**
269
     * @param string $colour
270
     * @return $this
271
     */
272
    public function setBackgroundColour($colour)
273
    {
274
        Assertion::inArray($colour, MenuStyle::getAvailableColours());
275
276
        $this->style['bg'] = $colour;
277
278
        return $this;
279
    }
280
281
    /**
282
     * @param string $colour
283
     * @return $this
284
     */
285
    public function setForegroundColour($colour)
286
    {
287
        Assertion::inArray($colour, MenuStyle::getAvailableColours());
288
289
        $this->style['fg'] = $colour;
290
291
        return $this;
292
    }
293
294
    /**
295
     * @param int $width
296
     * @return $this
297
     */
298
    public function setWidth($width)
299
    {
300
        Assertion::integer($width);
301
302
        $this->style['width'] = $width;
303
304
        return $this;
305
    }
306
307
    /**
308
     * @param int $padding
309
     * @return $this
310
     */
311
    public function setPadding($padding)
312
    {
313
        Assertion::integer($padding);
314
315
        $this->style['padding'] = $padding;
316
317
        return $this;
318
    }
319
320
    /**
321
     * @param int $margin
322
     * @return $this
323
     */
324
    public function setMargin($margin)
325
    {
326
        Assertion::integer($margin);
327
328
        $this->style['margin'] = $margin;
329
330
        return $this;
331
    }
332
333
    /**
334
     * @param string $marker
335
     * @return $this
336
     */
337
    public function setUnselectedMarker($marker)
338
    {
339
        Assertion::string($marker);
340
341
        $this->style['unselectedMarker'] = $marker;
342
343
        return $this;
344
    }
345
346
    /**
347
     * @param string $marker
348
     * @return $this
349
     */
350
    public function setSelectedMarker($marker)
351
    {
352
        Assertion::string($marker);
353
354
        $this->style['selectedMarker'] = $marker;
355
356
        return $this;
357
    }
358
359
    /**
360
     * @param string $extra
361
     * @return $this
362
     */
363
    public function setItemExtra($extra)
364
    {
365
        Assertion::string($extra);
366
367
        $this->style['itemExtra'] = $extra;
368
369
        return $this;
370
    }
371
372
    /**
373
     * @param string $separator
374
     * @return $this
375
     */
376
    public function setTitleSeparator($separator)
377
    {
378
        Assertion::string($separator);
379
380
        $this->style['titleSeparator'] = $separator;
381
382
        return $this;
383
    }
384
385
    /**
386
     * @param TerminalInterface $terminal
387
     * @return $this
388
     */
389
    public function setTerminal(TerminalInterface $terminal)
390
    {
391
        $this->terminal = $terminal;
392
        $this->style['terminal'] = $this->terminal;
393
        return $this;
394
    }
395
396
    /**
397
     * @return array
398
     */
399
    private function getDefaultItems()
400
    {
401
        $actions = [];
402
        if ($this->parent) {
403
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
404
        }
405
        
406
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
407
        return $actions;
408
    }
409
410
    /**
411
     * @return $this
412
     */
413
    public function disableDefaultItems()
414
    {
415
        $this->disableDefaultItems = true;
416
417
        return $this;
418
    }
419
420
    /**
421
     * @param array $items
422
     * @return bool
423
     */
424
    private function itemsHaveExtra(array $items)
425
    {
426
        return !empty(array_filter($items, function (MenuItemInterface $item) {
427
            return $item->showsItemExtra();
428
        }));
429
    }
430
431
    /**
432
     * Recursively drop back to the parents menu style
433
     * when the current menu has a parent and has no changes
434
     *
435
     * @return MenuStyle
436
     */
437
    private function getMenuStyle()
438
    {
439
        $diff = array_udiff_assoc($this->style, $this->getStyleClassDefaults(), function ($current, $default) {
440
            if ($current instanceof TerminalInterface) {
441
                return 0;
442
            }
443
444
            return $current === $default ? 0 : 1;
445
        });
446
447
        if (!$diff && null !== $this->parent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $diff of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
448
            return $this->parent->getMenuStyle();
449
        }
450
        
451
        return new MenuStyle(...array_values($this->style));
0 ignored issues
show
Documentation introduced by
array_values($this->style) is of type array<integer,?>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
452
    }
453
454
    /**
455
     * Return to parent builder
456
     *
457
     * @return CliMenuBuilder
458
     * @throws RuntimeException
459
     */
460
    public function end()
461
    {
462
        if (null === $this->parent) {
463
            throw new RuntimeException('No parent builder to return to');
464
        }
465
466
        return $this->parent;
467
    }
468
469
    /**
470
     * @param string $id
471
     * @return CliMenuBuilder
472
     * @throws RuntimeException
473
     */
474
    public function getSubMenu($id)
475
    {
476
        if (false === $this->isBuilt) {
477
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
478
        }
479
480
        return $this->subMenus[$id];
481
    }
482
483
    /**
484
     * @param array $items
485
     * @return array
486
     */
487
    private function buildSubMenus(array $items)
488
    {
489
        return array_map(function ($item) {
490
            if (!is_string($item)) {
491
                return $item;
492
            }
493
494
            $menuBuilder           = $this->subMenus[$item];
495
            $this->subMenus[$item] = $menuBuilder->build();
496
497
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
0 ignored issues
show
Bug introduced by
It seems like $this->subMenus[$item] can also be of type object<PhpSchool\CliMenu\CliMenuBuilder>; however, PhpSchool\CliMenu\MenuIt...MenuItem::__construct() does only seem to accept object<PhpSchool\CliMenu\CliMenu>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
498
        }, $items);
499
    }
500
501
    /**
502
     * @return CliMenu
503
     */
504
    public function build()
505
    {
506
        $this->isBuilt = true;
507
508
        $mergedItems = $this->disableDefaultItems
509
            ? $this->menuItems
510
            : array_merge($this->menuItems, $this->getDefaultItems());
511
512
        $menuItems = $this->buildSubMenus($mergedItems);
513
514
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
515
516
        $menu = new CliMenu(
517
            $this->menuTitle ?: false,
0 ignored issues
show
Security Bug introduced by
It seems like $this->menuTitle ?: false can also be of type false; however, PhpSchool\CliMenu\CliMenu::__construct() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
518
            $menuItems,
519
            $this->terminal,
520
            $this->getMenuStyle()
521
        );
522
        
523
        foreach ($this->subMenus as $subMenu) {
524
            $subMenu->setParent($menu);
0 ignored issues
show
Bug introduced by
The method setParent() does not seem to exist on object<PhpSchool\CliMenu\CliMenuBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
525
        }
526
527
        return $menu;
528
    }
529
}
530