Completed
Push — master ( df8184...e3b994 )
by Aydin
02:31
created

src/Builder/CliMenuBuilder.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\MenuItem\AsciiArtItem;
8
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
9
use PhpSchool\CliMenu\MenuItem\SelectableItem;
10
use PhpSchool\CliMenu\MenuItem\SplitItem;
11
use PhpSchool\CliMenu\CliMenu;
12
use PhpSchool\CliMenu\MenuStyle;
13
use PhpSchool\CliMenu\Terminal\TerminalFactory;
14
use PhpSchool\CliMenu\Util\ColourUtil;
15
use PhpSchool\Terminal\Terminal;
16
use RuntimeException;
17
18
/**
19
 * @author Michael Woodward <[email protected]>
20
 * @author Aydin Hassan <[email protected]>
21
 */
22
class CliMenuBuilder implements Builder
23
{
24
    use BuilderUtils;
25
26
    /**
27
     * @var null|Builder
28
     */
29
    private $parent;
30
    
31
    /**
32
     * @var bool
33
     */
34
    private $isBuilt = false;
35
36
    /**
37
     * @var SplitItemBuilder[]
38
     */
39
    private $splitItemBuilders = [];
40
41
    /**
42
     * @var SplitItem[]
43
     */
44
    private $splitItems = [];
45
46
    /**
47
     * @var string
48
     */
49
    private $goBackButtonText = 'Go Back';
50
51
    /**
52
     * @var string
53
     */
54
    private $exitButtonText = 'Exit';
55
56
    /**
57
     * @var array
58
     */
59
    private $style;
60
61
    /**
62
     * @var Terminal
63
     */
64
    private $terminal;
65
66
    /**
67
     * @var string
68
     */
69
    private $menuTitle;
70
71
    /**
72
     * @var bool
73
     */
74
    private $disableDefaultItems = false;
75
76
    /**
77
     * @var bool
78
     */
79
    private $disabled = false;
80
81
    public function __construct(Builder $parent = null)
82
    {
83
        $this->parent   = $parent;
84
        $this->terminal = $this->parent !== null
85
            ? $this->parent->getTerminal()
86
            : TerminalFactory::fromSystem();
87
        $this->style = MenuStyle::getDefaultStyleValues();
88
    }
89
90
    public function setTitle(string $title) : self
91
    {
92
        $this->menuTitle = $title;
93
94
        return $this;
95
    }
96
97
    public function addMenuItem(MenuItemInterface $item) : self
98
    {
99
        $this->menuItems[] = $item;
100
101
        return $this;
102
    }
103
104
    public function addItems(array $items) : self
105
    {
106
        foreach ($items as $item) {
107
            $this->addItem(...$item);
0 ignored issues
show
The call to addItem() misses a required argument $itemCallable.

This check looks for function calls that miss required arguments.

Loading history...
108
        }
109
110
        return $this;
111
    }
112
113
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
114
    {
115
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
116
117
        return $this;
118
    }
119
120
    /**
121
     * Add a split item
122
     */
123
    public function addSplitItem() : SplitItemBuilder
124
    {
125
        $this->menuItems[] = [
126
            'type' => 'splititem-placeholder',
127
            'id'   => $id = uniqid('', true),
128
        ];
129
                
130
        $this->splitItemBuilders[$id] = new SplitItemBuilder($this);
131
        return $this->splitItemBuilders[$id];
132
    }
133
134
    /**
135
     * Disable a submenu
136
     *
137
     * @throws \InvalidArgumentException
138
     */
139
    public function disableMenu() : self
140
    {
141
        if (!$this->parent) {
142
            throw new \InvalidArgumentException(
143
                'You can\'t disable the root menu'
144
            );
145
        }
146
147
        $this->disabled = true;
148
149
        return $this;
150
    }
151
152
    public function isMenuDisabled() : bool
153
    {
154
        return $this->disabled;
155
    }
156
157
    public function setGoBackButtonText(string $goBackButtonTest) : self
158
    {
159
        $this->goBackButtonText = $goBackButtonTest;
160
161
        return $this;
162
    }
163
164
    public function setExitButtonText(string $exitButtonText) : self
165
    {
166
        $this->exitButtonText = $exitButtonText;
167
168
        return $this;
169
    }
170
171
    public function setBackgroundColour(string $colour, string $fallback = null) : self
172
    {
173
        $this->style['bg'] = ColourUtil::validateColour(
174
            $this->terminal,
175
            $colour,
176
            $fallback
177
        );
178
179
        return $this;
180
    }
181
182
    public function setForegroundColour(string $colour, string $fallback = null) : self
183
    {
184
        $this->style['fg'] = ColourUtil::validateColour(
185
            $this->terminal,
186
            $colour,
187
            $fallback
188
        );
189
190
        return $this;
191
    }
192
193
    public function setWidth(int $width) : self
194
    {
195
        $this->style['width'] = $width;
196
197
        return $this;
198
    }
199
200
    public function setPadding(int $topBottom, int $leftRight = null) : self
201
    {
202
        if ($leftRight === null) {
203
            $leftRight = $topBottom;
204
        }
205
206
        $this->setPaddingTopBottom($topBottom);
207
        $this->setPaddingLeftRight($leftRight);
208
209
        return $this;
210
    }
211
212
    public function setPaddingTopBottom(int $topBottom) : self
213
    {
214
        $this->style['paddingTopBottom'] = $topBottom;
215
216
        return $this;
217
    }
218
219
    public function setPaddingLeftRight(int $leftRight) : self
220
    {
221
        $this->style['paddingLeftRight'] = $leftRight;
222
223
        return $this;
224
    }
225
226
    public function setMarginAuto() : self
227
    {
228
        $this->style['marginAuto'] = true;
229
230
        return $this;
231
    }
232
233
    public function setMargin(int $margin) : self
234
    {
235
        $this->style['marginAuto'] = false;
236
        $this->style['margin'] = $margin;
237
238
        return $this;
239
    }
240
241
    public function setUnselectedMarker(string $marker) : self
242
    {
243
        $this->style['unselectedMarker'] = $marker;
244
245
        return $this;
246
    }
247
248
    public function setSelectedMarker(string $marker) : self
249
    {
250
        $this->style['selectedMarker'] = $marker;
251
252
        return $this;
253
    }
254
255
    public function setItemExtra(string $extra) : self
256
    {
257
        $this->style['itemExtra'] = $extra;
258
259
        return $this;
260
    }
261
262
    public function setTitleSeparator(string $separator) : self
263
    {
264
        $this->style['titleSeparator'] = $separator;
265
266
        return $this;
267
    }
268
269
    public function setBorder(
270
        int $topWidth,
271
        $rightWidth = null,
272
        $bottomWidth = null,
273
        $leftWidth = null,
274
        string $colour = null
275
    ) : self {
276
        if (!is_int($rightWidth)) {
277
            $colour = $rightWidth;
278
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
279
        } elseif (!is_int($bottomWidth)) {
280
            $colour = $bottomWidth;
281
            $bottomWidth = $topWidth;
282
            $leftWidth = $rightWidth;
283
        } elseif (!is_int($leftWidth)) {
284
            $colour = $leftWidth;
285
            $leftWidth = $rightWidth;
286
        }
287
288
        $this->style['borderTopWidth'] = $topWidth;
289
        $this->style['borderRightWidth'] = $rightWidth;
290
        $this->style['borderBottomWidth'] = $bottomWidth;
291
        $this->style['borderLeftWidth'] = $leftWidth;
292
293
        if (is_string($colour)) {
294
            $this->style['borderColour'] = $colour;
295
        } elseif ($colour !== null) {
296
            throw new \InvalidArgumentException('Invalid colour');
297
        }
298
299
        return $this;
300
    }
301
302
    public function setBorderTopWidth(int $width) : self
303
    {
304
        $this->style['borderTopWidth'] = $width;
305
        
306
        return $this;
307
    }
308
309
    public function setBorderRightWidth(int $width) : self
310
    {
311
        $this->style['borderRightWidth'] = $width;
312
313
        return $this;
314
    }
315
316
    public function setBorderBottomWidth(int $width) : self
317
    {
318
        $this->style['borderBottomWidth'] = $width;
319
320
        return $this;
321
    }
322
323
    public function setBorderLeftWidth(int $width) : self
324
    {
325
        $this->style['borderLeftWidth'] = $width;
326
327
        return $this;
328
    }
329
330
    public function setBorderColour(string $colour, $fallback = null) : self
331
    {
332
        $this->style['borderColour'] = $colour;
333
        $this->style['borderColourFallback'] = $fallback;
334
335
        return $this;
336
    }
337
338
    public function setTerminal(Terminal $terminal) : self
339
    {
340
        $this->terminal = $terminal;
341
        return $this;
342
    }
343
344
    public function getTerminal() : Terminal
345
    {
346
        return $this->terminal;
347
    }
348
349
    private function getDefaultItems() : array
350
    {
351
        $actions = [];
352
        if ($this->parent) {
353
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
354
        }
355
356
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
357
        return $actions;
358
    }
359
360
    public function disableDefaultItems() : self
361
    {
362
        $this->disableDefaultItems = true;
363
364
        return $this;
365
    }
366
367
    private function itemsHaveExtra(array $items) : bool
368
    {
369
        return !empty(array_filter($items, function (MenuItemInterface $item) {
370
            return $item->showsItemExtra();
371
        }));
372
    }
373
374
    /**
375
     * Recursively drop back to the parents menu style
376
     * when the current menu has a parent and has no changes
377
     */
378
    public function getMenuStyle() : MenuStyle
379
    {
380
        if (null === $this->parent) {
381
            return $this->buildStyle();
382
        }
383
384
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
385
            return $this->buildStyle();
386
        }
387
388
        return $this->parent->getMenuStyle();
389
    }
390
391
    private function buildStyle() : MenuStyle
392
    {
393
        $style = (new MenuStyle($this->terminal))
394
            ->setFg($this->style['fg'])
395
            ->setBg($this->style['bg'])
396
            ->setWidth($this->style['width'])
397
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
398
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
399
            ->setSelectedMarker($this->style['selectedMarker'])
400
            ->setUnselectedMarker($this->style['unselectedMarker'])
401
            ->setItemExtra($this->style['itemExtra'])
402
            ->setDisplaysExtra($this->style['displaysExtra'])
403
            ->setTitleSeparator($this->style['titleSeparator'])
404
            ->setBorderTopWidth($this->style['borderTopWidth'])
405
            ->setBorderRightWidth($this->style['borderRightWidth'])
406
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
407
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
408
            ->setBorderColour($this->style['borderColour'], $this->style['borderColourFallback']);
409
410
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
411
412
        return $style;
413
    }
414
415
    /**
416
     * @throws RuntimeException
417
     */
418
    public function getSubMenu(string $id) : CliMenu
419
    {
420
        if (false === $this->isBuilt) {
421
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
422
        }
423
424
        return $this->subMenus[$id];
425
    }
426
    
427
    private function buildSplitItems(array $items) : array
428
    {
429
        return array_map(function ($item) {
430
            if (!is_array($item) || $item['type'] !== 'splititem-placeholder') {
431
                return $item;
432
            }
433
434
            $splitItemBuilder        = $this->splitItemBuilders[$item['id']];
435
            $this->splitItems[$item['id']] = $splitItemBuilder->build();
436
437
            return $this->splitItems[$item['id']];
438
        }, $items);
439
    }
440
441
    public function build() : CliMenu
442
    {
443
        $this->isBuilt = true;
444
445
        $mergedItems = $this->disableDefaultItems
446
            ? $this->menuItems
447
            : array_merge($this->menuItems, $this->getDefaultItems());
448
449
        
450
        $menuItems = $this->buildSplitItems($mergedItems);
451
        $menuItems = $this->buildSubMenus($menuItems);
452
453
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
454
455
        $menu = new CliMenu(
456
            $this->menuTitle,
457
            $menuItems,
458
            $this->terminal,
459
            $this->getMenuStyle()
460
        );
461
462
        foreach ($this->subMenus as $subMenu) {
463
            $subMenu->setParent($menu);
464
        }
465
        
466
        foreach ($this->splitItemBuilders as $splitItemBuilder) {
467
            $splitItemBuilder->setSubMenuParents($menu);
468
        }
469
470
        return $menu;
471
    }
472
473
    /**
474
     * Return to parent builder
475
     *
476
     * @return CliMenuBuilder|SplitItemBuilder
477
     */
478
    public function end() : ?Builder
479
    {
480
        if (null === $this->parent) {
481
            throw new RuntimeException('No parent builder to return to');
482
        }
483
484
        return $this->parent;
485
    }
486
}
487