Completed
Push — master ( 761de5...d7bebf )
by Aydin
12s
created

CliMenuBuilder   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 435
Duplicated Lines 4.83 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 12
dl 21
loc 435
rs 7.4757
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setTitle() 0 6 1
A addMenuItem() 0 6 1
A addItem() 0 10 1
A addItems() 0 8 2
A addStaticItem() 0 6 1
A addLineBreak() 0 6 1
A addAsciiArt() 0 6 1
A addSubMenu() 0 12 2
A disableMenu() 0 12 2
A isMenuDisabled() 0 4 1
A setGoBackButtonText() 0 6 1
A setExitButtonText() 0 6 1
A setForegroundColour() 10 10 1
A setWidth() 0 6 1
A setPadding() 0 6 1
A setBackgroundColour() 0 10 1
A setMarginAuto() 0 6 1
A setMargin() 0 9 1
A setUnselectedMarker() 0 6 1
A setSelectedMarker() 0 6 1
A setItemExtra() 0 6 1
A setTitleSeparator() 0 6 1
B setBorder() 11 32 6
A setTerminal() 0 5 1
A getTerminal() 0 4 1
A getDefaultItems() 0 10 2
A disableDefaultItems() 0 6 1
A itemsHaveExtra() 0 6 1
A getMenuStyle() 0 12 3
A buildStyle() 0 22 2
A end() 0 8 2
A getSubMenu() 0 8 2
A buildSubMenus() 0 13 2
B build() 0 25 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CliMenuBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CliMenuBuilder, and based on these observations, apply Extract Interface, too.

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
        } 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