Completed
Pull Request — master (#127)
by Aydin
01:52
created

CliMenuBuilder::buildSubMenus()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 1
nop 1
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\SplitItem;
13
use PhpSchool\CliMenu\MenuItem\StaticItem;
14
use PhpSchool\CliMenu\Terminal\TerminalFactory;
15
use PhpSchool\CliMenu\Util\ColourUtil;
16
use PhpSchool\Terminal\Terminal;
17
use RuntimeException;
18
19
/**
20
 * @author Michael Woodward <[email protected]>
21
 * @author Aydin Hassan <[email protected]>
22
 */
23
class CliMenuBuilder implements Builder
24
{
25
    use BuilderUtils;
26
    
27
    /**
28
     * @var bool
29
     */
30
    private $isBuilt = false;
31
32
    /**
33
     * @var SplitItemBuilder[]
34
     */
35
    private $splitItemBuilders = [];
36
37
    /**
38
     * @var SplitItem[]
39
     */
40
    private $splitItems = [];
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 $style;
56
57
    /**
58
     * @var Terminal
59
     */
60
    private $terminal;
61
62
    /**
63
     * @var string
64
     */
65
    private $menuTitle;
66
67
    /**
68
     * @var bool
69
     */
70
    private $disableDefaultItems = false;
71
72
    /**
73
     * @var bool
74
     */
75
    private $disabled = false;
76
77
    public function __construct(Builder $parent = null)
78
    {
79
        $this->parent   = $parent;
80
        $this->terminal = $this->parent !== null
81
            ? $this->parent->getTerminal()
82
            : TerminalFactory::fromSystem();
83
        $this->style = MenuStyle::getDefaultStyleValues();
84
    }
85
86
    public function setTitle(string $title) : self
87
    {
88
        $this->menuTitle = $title;
89
90
        return $this;
91
    }
92
93
    public function addMenuItem(MenuItemInterface $item) : self
94
    {
95
        $this->menuItems[] = $item;
96
97
        return $this;
98
    }
99
100
    public function addItems(array $items) : self
101
    {
102
        foreach ($items as $item) {
103
            $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...
104
        }
105
106
        return $this;
107
    }
108
109
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
110
    {
111
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
112
113
        return $this;
114
    }
115
116
    /**
117
     * Injects a submenu directly (without going through the builder
118
     */
119
    public function injectSubMenu(string $id, CliMenu $subMenu) : CliMenuBuilder
120
    {
121
        $this->subMenus[$id] = $subMenu;
122
123
        return $this;
124
    }
125
126
    /**
127
     * Add a split item
128
     */
129
    public function addSplitItem() : SplitItemBuilder
130
    {
131
        $this->menuItems[] = $id = uniqid('splititem-placeholder-', true);
132
        
133
        $this->splitItemBuilders[$id] = new SplitItemBuilder($this);
134
        return $this->splitItemBuilders[$id];
135
    }
136
137
    /**
138
     * Disable a submenu
139
     *
140
     * @throws \InvalidArgumentException
141
     */
142
    public function disableMenu() : self
143
    {
144
        if (!$this->parent) {
145
            throw new \InvalidArgumentException(
146
                'You can\'t disable the root menu'
147
            );
148
        }
149
150
        $this->disabled = true;
151
152
        return $this;
153
    }
154
155
    public function isMenuDisabled() : bool
156
    {
157
        return $this->disabled;
158
    }
159
160
    public function setGoBackButtonText(string $goBackButtonTest) : self
161
    {
162
        $this->goBackButtonText = $goBackButtonTest;
163
164
        return $this;
165
    }
166
167
    public function setExitButtonText(string $exitButtonText) : self
168
    {
169
        $this->exitButtonText = $exitButtonText;
170
171
        return $this;
172
    }
173
174
    public function setBackgroundColour(string $colour, string $fallback = null) : self
175
    {
176
        $this->style['bg'] = ColourUtil::validateColour(
177
            $this->terminal,
178
            $colour,
179
            $fallback
180
        );
181
182
        return $this;
183
    }
184
185 View Code Duplication
    public function setForegroundColour(string $colour, string $fallback = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
186
    {
187
        $this->style['fg'] = ColourUtil::validateColour(
188
            $this->terminal,
189
            $colour,
190
            $fallback
191
        );
192
193
        return $this;
194
    }
195
196
    public function setWidth(int $width) : self
197
    {
198
        $this->style['width'] = $width;
199
200
        return $this;
201
    }
202
203 View Code Duplication
    public function setPadding(int $topBottom, int $leftRight = null) : self
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
    {
205
        if ($leftRight === null) {
206
            $leftRight = $topBottom;
207
        }
208
209
        $this->setPaddingTopBottom($topBottom);
210
        $this->setPaddingLeftRight($leftRight);
211
212
        return $this;
213
    }
214
215
    public function setPaddingTopBottom(int $topBottom) : self
216
    {
217
        $this->style['paddingTopBottom'] = $topBottom;
218
219
        return $this;
220
    }
221
222
    public function setPaddingLeftRight(int $leftRight) : self
223
    {
224
        $this->style['paddingLeftRight'] = $leftRight;
225
226
        return $this;
227
    }
228
229
    public function setMarginAuto() : self
230
    {
231
        $this->style['marginAuto'] = true;
232
233
        return $this;
234
    }
235
236
    public function setMargin(int $margin) : self
237
    {
238
        $this->style['marginAuto'] = false;
239
        $this->style['margin'] = $margin;
240
241
        return $this;
242
    }
243
244
    public function setUnselectedMarker(string $marker) : self
245
    {
246
        $this->style['unselectedMarker'] = $marker;
247
248
        return $this;
249
    }
250
251
    public function setSelectedMarker(string $marker) : self
252
    {
253
        $this->style['selectedMarker'] = $marker;
254
255
        return $this;
256
    }
257
258
    public function setItemExtra(string $extra) : self
259
    {
260
        $this->style['itemExtra'] = $extra;
261
262
        return $this;
263
    }
264
265
    public function setTitleSeparator(string $separator) : self
266
    {
267
        $this->style['titleSeparator'] = $separator;
268
269
        return $this;
270
    }
271
272
    public function setBorder(
273
        int $topWidth,
274
        $rightWidth = null,
275
        $bottomWidth = null,
276
        $leftWidth = null,
277
        string $colour = null
278
    ) : self {
279 View Code Duplication
        if (!is_int($rightWidth)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
            $colour = $rightWidth;
281
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
282
        } elseif (!is_int($bottomWidth)) {
283
            $colour = $bottomWidth;
284
            $bottomWidth = $topWidth;
285
            $leftWidth = $rightWidth;
286
        } elseif (!is_int($leftWidth)) {
287
            $colour = $leftWidth;
288
            $leftWidth = $rightWidth;
289
        }
290
291
        $this->style['borderTopWidth'] = $topWidth;
292
        $this->style['borderRightWidth'] = $rightWidth;
293
        $this->style['borderBottomWidth'] = $bottomWidth;
294
        $this->style['borderLeftWidth'] = $leftWidth;
295
296
        if (is_string($colour)) {
297
            $this->style['borderColour'] = $colour;
298
        } elseif ($colour !== null) {
299
            throw new \InvalidArgumentException('Invalid colour');
300
        }
301
302
        return $this;
303
    }
304
305
    public function setBorderTopWidth(int $width) : self
306
    {
307
        $this->style['borderTopWidth'] = $width;
308
        
309
        return $this;
310
    }
311
312
    public function setBorderRightWidth(int $width) : self
313
    {
314
        $this->style['borderRightWidth'] = $width;
315
316
        return $this;
317
    }
318
319
    public function setBorderBottomWidth(int $width) : self
320
    {
321
        $this->style['borderBottomWidth'] = $width;
322
323
        return $this;
324
    }
325
326
    public function setBorderLeftWidth(int $width) : self
327
    {
328
        $this->style['borderLeftWidth'] = $width;
329
330
        return $this;
331
    }
332
333
    public function setBorderColour(string $colour, $fallback = null) : self
334
    {
335
        $this->style['borderColour'] = $colour;
336
        $this->style['borderColourFallback'] = $fallback;
337
338
        return $this;
339
    }
340
341
    public function setTerminal(Terminal $terminal) : self
342
    {
343
        $this->terminal = $terminal;
344
        return $this;
345
    }
346
347
    public function getTerminal() : Terminal
348
    {
349
        return $this->terminal;
350
    }
351
352
    private function getDefaultItems() : array
353
    {
354
        $actions = [];
355
        if ($this->parent) {
356
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
357
        }
358
359
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
360
        return $actions;
361
    }
362
363
    public function disableDefaultItems() : self
364
    {
365
        $this->disableDefaultItems = true;
366
367
        return $this;
368
    }
369
370
    private function itemsHaveExtra(array $items) : bool
371
    {
372
        return !empty(array_filter($items, function (MenuItemInterface $item) {
373
            return $item->showsItemExtra();
374
        }));
375
    }
376
377
    /**
378
     * Recursively drop back to the parents menu style
379
     * when the current menu has a parent and has no changes
380
     */
381
    public function getMenuStyle() : MenuStyle
382
    {
383
        if (null === $this->parent) {
384
            return $this->buildStyle();
385
        }
386
387
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
388
            return $this->buildStyle();
389
        }
390
391
        return $this->parent->getMenuStyle();
392
    }
393
394
    private function buildStyle() : MenuStyle
395
    {
396
        $style = (new MenuStyle($this->terminal))
397
            ->setFg($this->style['fg'])
398
            ->setBg($this->style['bg'])
399
            ->setWidth($this->style['width'])
400
            ->setPaddingTopBottom($this->style['paddingTopBottom'])
401
            ->setPaddingLeftRight($this->style['paddingLeftRight'])
402
            ->setSelectedMarker($this->style['selectedMarker'])
403
            ->setUnselectedMarker($this->style['unselectedMarker'])
404
            ->setItemExtra($this->style['itemExtra'])
405
            ->setDisplaysExtra($this->style['displaysExtra'])
406
            ->setTitleSeparator($this->style['titleSeparator'])
407
            ->setBorderTopWidth($this->style['borderTopWidth'])
408
            ->setBorderRightWidth($this->style['borderRightWidth'])
409
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
410
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
411
            ->setBorderColour($this->style['borderColour'], $this->style['borderColourFallback']);
412
413
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
414
415
        return $style;
416
    }
417
418
    /**
419
     * @throws RuntimeException
420
     */
421
    public function getSubMenu(string $id) : CliMenu
422
    {
423
        if (false === $this->isBuilt) {
424
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
425
        }
426
427
        return $this->subMenus['submenu-placeholder-' . $id];
428
    }
429
    
430
    private function buildSplitItems(array $items) : array
431
    {
432
        return array_map(function ($item) {
433
            if (!is_string($item) || 0 !== strpos($item, 'splititem-placeholder-')) {
434
                return $item;
435
            }
436
437
            $splitItemBuilder        = $this->splitItemBuilders[$item];
438
            $this->splitItems[$item] = $splitItemBuilder->build();
439
440
            return $this->splitItems[$item];
441
        }, $items);
442
    }
443
444
    public function build() : CliMenu
445
    {
446
        $this->isBuilt = true;
447
448
        $mergedItems = $this->disableDefaultItems
449
            ? $this->menuItems
450
            : array_merge($this->menuItems, $this->getDefaultItems());
451
452
        
453
        $menuItems = $this->buildSplitItems($mergedItems);
454
        $menuItems = $this->buildSubMenus($menuItems);
455
456
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
457
458
        $menu = new CliMenu(
459
            $this->menuTitle,
460
            $menuItems,
461
            $this->terminal,
462
            $this->getMenuStyle()
463
        );
464
465
        foreach ($this->subMenus as $subMenu) {
466
            $subMenu->setParent($menu);
467
        }
468
        
469
        foreach ($this->splitItemBuilders as $splitItemBuilder) {
470
            $splitItemBuilder->setSubMenuParents($menu);
471
        }
472
473
        return $menu;
474
    }
475
}
476