Completed
Pull Request — master (#100)
by
unknown
03:28
created

CliMenuBuilder::setBorder()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 32
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
cc 6
eloc 26
nc 12
nop 5
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\Util\ColourUtil;
15
use Assert\Assertion;
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
24
{
25
    /**
26
     * @var bool
27
     */
28
    private $isBuilt = false;
29
30
    /**
31
     * @var null|self
32
     */
33
    private $parent;
34
    
35
    /**
36
     * @var self[]
37
     */
38
    private $subMenuBuilders = [];
39
40
    /**
41
     * @var CliMenu[]
42
     */
43
    private $subMenus = [];
44
45
    /**
46
     * @var string
47
     */
48
    private $goBackButtonText = 'Go Back';
49
    
50
    /**
51
     * @var string
52
     */
53
    private $exitButtonText = 'Exit';
54
55
    /**
56
     * @var array
57
     */
58
    private $menuItems = [];
59
60
    /**
61
     * @var array
62
     */
63
    private $style;
64
65
    /**
66
     * @var Terminal
67
     */
68
    private $terminal;
69
70
    /**
71
     * @var string
72
     */
73
    private $menuTitle = null;
74
75
    /**
76
     * @var bool
77
     */
78
    private $disableDefaultItems = false;
79
80
    /**
81
     * @var bool
82
     */
83
    private $disabled = false;
84
85
    public function __construct(CliMenuBuilder $parent = null)
86
    {
87
        $this->parent   = $parent;
88
        $this->terminal = $this->parent !== null
89
            ? $this->parent->getTerminal()
90
            : TerminalFactory::fromSystem();
91
        $this->style    = MenuStyle::getDefaultStyleValues();
92
    }
93
94
    public function setTitle(string $title) : self
95
    {
96
        $this->menuTitle = $title;
97
98
        return $this;
99
    }
100
101
    public function addMenuItem(MenuItemInterface $item) : self
102
    {
103
        $this->menuItems[] = $item;
104
105
        return $this;
106
    }
107
108
    public function addItem(
109
        string $text,
110
        callable $itemCallable,
111
        bool $showItemExtra = false,
112
        bool $disabled = false
113
    ) : self {
114
        $this->addMenuItem(new SelectableItem($text, $itemCallable, $showItemExtra, $disabled));
115
116
        return $this;
117
    }
118
119
    public function addItems(array $items) : self
120
    {
121
        foreach ($items as $item) {
122
            $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...
123
        }
124
        
125
        return $this;
126
    }
127
128
    public function addStaticItem(string $text) : self
129
    {
130
        $this->addMenuItem(new StaticItem($text));
131
132
        return $this;
133
    }
134
135
    public function addLineBreak(string $breakChar = ' ', int $lines = 1) : self
136
    {
137
        $this->addMenuItem(new LineBreakItem($breakChar, $lines));
138
139
        return $this;
140
    }
141
142
    public function addAsciiArt(string $art, string $position = AsciiArtItem::POSITION_CENTER, string $alt = '') : self
143
    {
144
        $this->addMenuItem(new AsciiArtItem($art, $position, $alt));
145
146
        return $this;
147
    }
148
149
    /**
150
     * Add a submenu with a string identifier
151
     */
152
    public function addSubMenu(string $id, CliMenuBuilder $subMenuBuilder = null) : CliMenuBuilder
153
    {
154
        $this->menuItems[]  = $id;
155
        
156
        if (null === $subMenuBuilder) {
157
            $this->subMenuBuilders[$id] = new static($this);
158
            return $this->subMenuBuilders[$id];
159
        }
160
        
161
        $this->subMenuBuilders[$id] = $subMenuBuilder;
162
        return $this;
163
    }
164
165
    /**
166
     * Disable a submenu
167
     *
168
     * @throws \InvalidArgumentException
169
     */
170
    public function disableMenu() : self
171
    {
172
        if (!$this->parent) {
173
            throw new \InvalidArgumentException(
174
                'You can\'t disable the root menu'
175
            );
176
        }
177
178
        $this->disabled = true;
179
180
        return $this;
181
    }
182
183
    public function isMenuDisabled() : bool
184
    {
185
        return $this->disabled;
186
    }
187
188
    public function setGoBackButtonText(string $goBackButtonTest) : self
189
    {
190
        $this->goBackButtonText = $goBackButtonTest;
191
        
192
        return $this;
193
    }
194
195
    public function setExitButtonText(string $exitButtonText) : self
196
    {
197
        $this->exitButtonText = $exitButtonText;
198
        
199
        return $this;
200
    }
201
202
    public function setBackgroundColour(string $colour, string $fallback = null) : self
203
    {
204
        $this->style['bg'] = ColourUtil::validateColour(
205
            $this->terminal,
206
            $colour,
207
            $fallback
208
        );
209
210
        return $this;
211
    }
212
213
    public function setForegroundColour(string $colour, string $fallback = null) : self
214
    {
215
        $this->style['fg'] = ColourUtil::validateColour(
216
            $this->terminal,
217
            $colour,
218
            $fallback
219
        );
220
221
        return $this;
222
    }
223
224
    public function setWidth(int $width) : self
225
    {
226
        $this->style['width'] = $width;
227
228
        return $this;
229
    }
230
231
    public function setPadding(int $padding) : self
232
    {
233
        $this->style['padding'] = $padding;
234
235
        return $this;
236
    }
237
238
    public function setMarginAuto() : self
239
    {
240
        $this->style['marginAuto'] = true;
241
        
242
        return $this;
243
    }
244
245
    public function setMargin(int $margin) : self
246
    {
247
        Assertion::greaterOrEqualThan($margin, 0);
248
        
249
        $this->style['marginAuto'] = false;
250
        $this->style['margin'] = $margin;
251
252
        return $this;
253
    }
254
255
    public function setUnselectedMarker(string $marker) : self
256
    {
257
        $this->style['unselectedMarker'] = $marker;
258
259
        return $this;
260
    }
261
262
    public function setSelectedMarker(string $marker) : self
263
    {
264
        $this->style['selectedMarker'] = $marker;
265
266
        return $this;
267
    }
268
269
    public function setItemExtra(string $extra) : self
270
    {
271
        $this->style['itemExtra'] = $extra;
272
273
        return $this;
274
    }
275
276
    public function setTitleSeparator(string $separator) : self
277
    {
278
        $this->style['titleSeparator'] = $separator;
279
280
        return $this;
281
    }
282
283
    public function setBorder(
284
        int $topWidth,
285
        $rightWidth = null,
286
        $bottomWidth = null,
287
        $leftWidth = null,
288
        string $colour = null
289
    ) : self {
290
        if (!is_int($rightWidth)) {
291
            $colour = $rightWidth;
292
            $rightWidth = $bottomWidth = $leftWidth = $topWidth;
293
        } elseif (!is_int($bottomWidth)) {
294
            $colour = $bottomWidth;
295
            $bottomWidth = $topWidth;
296
            $leftWidth = $rightWidth;
297
        } elseif (!is_int($leftWidth)) {
298
            $colour = $leftWidth;
299
            $leftWidth = $rightWidth;
300
        }
301
302
        $this->style['borderTopWidth'] = $topWidth;
303
        $this->style['borderRightWidth'] = $rightWidth;
304
        $this->style['borderBottomWidth'] = $bottomWidth;
305
        $this->style['borderLeftWidth'] = $leftWidth;
306
307
        if (is_string($colour)) {
308
            $this->style['borderColour'] = $colour;
309
        } elseif ($colour !== null) {
310
            throw new \InvalidArgumentException('Invalid colour');
311
        }
312
313
        return $this;
314
    }
315
316
    public function setTerminal(Terminal $terminal) : self
317
    {
318
        $this->terminal = $terminal;
319
        return $this;
320
    }
321
322
    public function getTerminal() : Terminal
323
    {
324
        return $this->terminal;
325
    }
326
327
    private function getDefaultItems() : array
328
    {
329
        $actions = [];
330
        if ($this->parent) {
331
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
332
        }
333
        
334
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
335
        return $actions;
336
    }
337
338
    public function disableDefaultItems() : self
339
    {
340
        $this->disableDefaultItems = true;
341
342
        return $this;
343
    }
344
345
    private function itemsHaveExtra(array $items) : bool
346
    {
347
        return !empty(array_filter($items, function (MenuItemInterface $item) {
348
            return $item->showsItemExtra();
349
        }));
350
    }
351
352
    /**
353
     * Recursively drop back to the parents menu style
354
     * when the current menu has a parent and has no changes
355
     */
356
    private function getMenuStyle() : MenuStyle
357
    {
358
        if (null === $this->parent) {
359
            return $this->buildStyle();
360
        }
361
362
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
363
            return $this->buildStyle();
364
        }
365
366
        return $this->parent->getMenuStyle();
367
    }
368
369
    private function buildStyle() : MenuStyle
370
    {
371
        $style = (new MenuStyle($this->terminal))
372
            ->setFg($this->style['fg'])
373
            ->setBg($this->style['bg'])
374
            ->setWidth($this->style['width'])
375
            ->setPadding($this->style['padding'])
376
            ->setSelectedMarker($this->style['selectedMarker'])
377
            ->setUnselectedMarker($this->style['unselectedMarker'])
378
            ->setItemExtra($this->style['itemExtra'])
379
            ->setDisplaysExtra($this->style['displaysExtra'])
380
            ->setTitleSeparator($this->style['titleSeparator'])
381
            ->setBorderTopWidth($this->style['borderTopWidth'])
382
            ->setBorderRightWidth($this->style['borderRightWidth'])
383
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
384
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
385
            ->setBorderColour($this->style['borderColour']);
386
387
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
388
        
389
        return $style;
390
    }
391
392
    /**
393
     * Return to parent builder
394
     *
395
     * @throws RuntimeException
396
     */
397
    public function end() : CliMenuBuilder
398
    {
399
        if (null === $this->parent) {
400
            throw new RuntimeException('No parent builder to return to');
401
        }
402
403
        return $this->parent;
404
    }
405
406
    /**
407
     * @throws RuntimeException
408
     */
409
    public function getSubMenu(string $id) : CliMenu
410
    {
411
        if (false === $this->isBuilt) {
412
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
413
        }
414
415
        return $this->subMenus[$id];
416
    }
417
418
    private function buildSubMenus(array $items) : array
419
    {
420
        return array_map(function ($item) {
421
            if (!is_string($item)) {
422
                return $item;
423
            }
424
425
            $menuBuilder           = $this->subMenuBuilders[$item];
426
            $this->subMenus[$item] = $menuBuilder->build();
427
428
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
429
        }, $items);
430
    }
431
432
    public function build() : CliMenu
433
    {
434
        $this->isBuilt = true;
435
436
        $mergedItems = $this->disableDefaultItems
437
            ? $this->menuItems
438
            : array_merge($this->menuItems, $this->getDefaultItems());
439
440
        $menuItems = $this->buildSubMenus($mergedItems);
441
442
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
443
444
        $menu = new CliMenu(
445
            $this->menuTitle,
446
            $menuItems,
447
            $this->terminal,
448
            $this->getMenuStyle()
449
        );
450
        
451
        foreach ($this->subMenus as $subMenu) {
452
            $subMenu->setParent($menu);
453
        }
454
455
        return $menu;
456
    }
457
}
458