Completed
Pull Request — master (#100)
by
unknown
02:10
created

CliMenuBuilder::setBorder()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 30
Code Lines 24

Duplication

Lines 11
Ratio 36.67 %

Importance

Changes 0
Metric Value
dl 11
loc 30
rs 8.439
c 0
b 0
f 0
cc 5
eloc 24
nc 8
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 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...
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 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...
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
        }
310
311
        return $this;
312
    }
313
314
    public function setTerminal(Terminal $terminal) : self
315
    {
316
        $this->terminal = $terminal;
317
        return $this;
318
    }
319
320
    public function getTerminal() : Terminal
321
    {
322
        return $this->terminal;
323
    }
324
325
    private function getDefaultItems() : array
326
    {
327
        $actions = [];
328
        if ($this->parent) {
329
            $actions[] = new SelectableItem($this->goBackButtonText, new GoBackAction);
330
        }
331
        
332
        $actions[] = new SelectableItem($this->exitButtonText, new ExitAction);
333
        return $actions;
334
    }
335
336
    public function disableDefaultItems() : self
337
    {
338
        $this->disableDefaultItems = true;
339
340
        return $this;
341
    }
342
343
    private function itemsHaveExtra(array $items) : bool
344
    {
345
        return !empty(array_filter($items, function (MenuItemInterface $item) {
346
            return $item->showsItemExtra();
347
        }));
348
    }
349
350
    /**
351
     * Recursively drop back to the parents menu style
352
     * when the current menu has a parent and has no changes
353
     */
354
    private function getMenuStyle() : MenuStyle
355
    {
356
        if (null === $this->parent) {
357
            return $this->buildStyle();
358
        }
359
360
        if ($this->style !== MenuStyle::getDefaultStyleValues()) {
361
            return $this->buildStyle();
362
        }
363
364
        return $this->parent->getMenuStyle();
365
    }
366
367
    private function buildStyle() : MenuStyle
368
    {
369
        $style = (new MenuStyle($this->terminal))
370
            ->setFg($this->style['fg'])
371
            ->setBg($this->style['bg'])
372
            ->setWidth($this->style['width'])
373
            ->setPadding($this->style['padding'])
374
            ->setSelectedMarker($this->style['selectedMarker'])
375
            ->setUnselectedMarker($this->style['unselectedMarker'])
376
            ->setItemExtra($this->style['itemExtra'])
377
            ->setDisplaysExtra($this->style['displaysExtra'])
378
            ->setTitleSeparator($this->style['titleSeparator'])
379
            ->setBorderTopWidth($this->style['borderTopWidth'])
380
            ->setBorderRightWidth($this->style['borderRightWidth'])
381
            ->setBorderBottomWidth($this->style['borderBottomWidth'])
382
            ->setBorderLeftWidth($this->style['borderLeftWidth'])
383
            ->setBorderColour($this->style['borderColour']);
384
385
        $this->style['marginAuto'] ? $style->setMarginAuto() : $style->setMargin($this->style['margin']);
386
        
387
        return $style;
388
    }
389
390
    /**
391
     * Return to parent builder
392
     *
393
     * @throws RuntimeException
394
     */
395
    public function end() : CliMenuBuilder
396
    {
397
        if (null === $this->parent) {
398
            throw new RuntimeException('No parent builder to return to');
399
        }
400
401
        return $this->parent;
402
    }
403
404
    /**
405
     * @throws RuntimeException
406
     */
407
    public function getSubMenu(string $id) : CliMenu
408
    {
409
        if (false === $this->isBuilt) {
410
            throw new RuntimeException(sprintf('Menu: "%s" cannot be retrieved until menu has been built', $id));
411
        }
412
413
        return $this->subMenus[$id];
414
    }
415
416
    private function buildSubMenus(array $items) : array
417
    {
418
        return array_map(function ($item) {
419
            if (!is_string($item)) {
420
                return $item;
421
            }
422
423
            $menuBuilder           = $this->subMenuBuilders[$item];
424
            $this->subMenus[$item] = $menuBuilder->build();
425
426
            return new MenuMenuItem($item, $this->subMenus[$item], $menuBuilder->isMenuDisabled());
427
        }, $items);
428
    }
429
430
    public function build() : CliMenu
431
    {
432
        $this->isBuilt = true;
433
434
        $mergedItems = $this->disableDefaultItems
435
            ? $this->menuItems
436
            : array_merge($this->menuItems, $this->getDefaultItems());
437
438
        $menuItems = $this->buildSubMenus($mergedItems);
439
440
        $this->style['displaysExtra'] = $this->itemsHaveExtra($menuItems);
441
442
        $menu = new CliMenu(
443
            $this->menuTitle,
444
            $menuItems,
445
            $this->terminal,
446
            $this->getMenuStyle()
447
        );
448
        
449
        foreach ($this->subMenus as $subMenu) {
450
            $subMenu->setParent($menu);
451
        }
452
453
        return $menu;
454
    }
455
}
456