Completed
Push — master ( 1243e8...8ed409 )
by Aydin
02:16
created

CliMenu   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 329
Duplicated Lines 1.52 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 6
Bugs 4 Features 0
Metric Value
wmc 44
c 6
b 4
f 0
lcom 1
cbo 8
dl 5
loc 329
rs 8.3396

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setParent() 0 4 1
A getParent() 0 4 1
A getTerminal() 0 4 1
A isOpen() 0 4 1
A addItem() 0 4 1
A selectFirstItem() 0 9 3
B display() 0 17 6
B moveSelection() 0 19 6
A getSelectedItem() 0 4 1
A executeCurrentItem() 0 9 2
A draw() 0 21 2
B drawMenuItem() 0 27 4
A open() 0 10 2
A close() 0 11 2
A closeThis() 0 6 1
B __construct() 5 20 6
A configureTerminal() 0 8 1
A tearDownTerminal() 0 7 1
A assertTerminalIsValidTTY() 0 8 2

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 CliMenu 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 CliMenu, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpSchool\CliMenu;
4
5
use PhpSchool\CliMenu\Exception\InvalidInstantiationException;
6
use PhpSchool\CliMenu\Exception\InvalidTerminalException;
7
use PhpSchool\CliMenu\MenuItem\LineBreakItem;
8
use PhpSchool\CliMenu\MenuItem\MenuItem;
9
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
10
use PhpSchool\CliMenu\MenuItem\StaticItem;
11
use PhpSchool\CliMenu\Terminal\TerminalFactory;
12
use \PhpSchool\CliMenu\Terminal\TerminalInterface;
13
14
/**
15
 * Class CliMenu
16
 *
17
 * @package PhpSchool\CliMenu
18
 * @author Michael Woodward <[email protected]>
19
 */
20
class CliMenu
21
{
22
    /**
23
     * @var TerminalInterface
24
     */
25
    protected $terminal;
26
27
    /**
28
     * @var MenuStyle
29
     */
30
    protected $style;
31
32
    /**
33
     * @var string
34
     */
35
    protected $title;
36
37
    /**
38
     * @var MenuItemInterface[]
39
     */
40
    protected $items = [];
41
42
    /**
43
     * @var int
44
     */
45
    protected $selectedItem;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $open = false;
51
52
    /**
53
     * @var string
54
     */
55
    private $allowedConsumer = 'PhpSchool\CliMenu\CliMenuBuilder';
56
57
    /**
58
     * @var CliMenu|null
59
     */
60
    protected $parent;
61
62
    /**
63
     * @param string $title
64
     * @param array $items
65
     * @param TerminalInterface|null $terminal
66
     * @param MenuStyle|null $style
67
     * @throws InvalidInstantiationException
68
     * @throws InvalidTerminalException
69
     */
70
    public function __construct(
71
        $title,
72
        array $items,
73
        TerminalInterface $terminal = null,
74
        MenuStyle $style = null
75
    ) {
76
        $builder = debug_backtrace();
77 View Code Duplication
        if (count($builder) < 2 || !isset($builder[1]['class']) || $builder[1]['class'] !== $this->allowedConsumer) {
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...
78
            throw new InvalidInstantiationException(
79
                sprintf('The CliMenu must be instantiated by "%s"', $this->allowedConsumer)
80
            );
81
        }
82
83
        $this->title      = $title;
84
        $this->items      = $items;
85
        $this->terminal   = $terminal ?: TerminalFactory::fromSystem();
86
        $this->style      = $style ?: new MenuStyle();
87
88
        $this->selectFirstItem();
89
    }
90
91
    /**
92
     * Configure the terminal to work with CliMenu
93
     *
94
     * @throws InvalidTerminalException
95
     */
96
    protected function configureTerminal()
97
    {
98
        $this->assertTerminalIsValidTTY();
99
100
        $this->terminal->setCanonicalMode();
101
        $this->terminal->disableCursor();
102
        $this->terminal->clear();
103
    }
104
105
    /**
106
     * Revert changes made to the terminal
107
     *
108
     * @throws InvalidTerminalException
109
     */
110
    protected function tearDownTerminal()
111
    {
112
        $this->assertTerminalIsValidTTY();
113
114
        $this->terminal->setCanonicalMode(false);
115
        $this->terminal->enableCursor();
116
    }
117
118
    private function assertTerminalIsValidTTY()
119
    {
120
        if (!$this->terminal->isTTY()) {
121
            throw new InvalidTerminalException(
122
                sprintf('Terminal "%s" is not a valid TTY', $this->terminal->getDetails())
123
            );
124
        }
125
    }
126
127
    /**
128
     * @param CliMenu $parent
129
     */
130
    public function setParent(CliMenu $parent)
131
    {
132
        $this->parent = $parent;
133
    }
134
135
    /**
136
     * @return CliMenu|null
137
     */
138
    public function getParent()
139
    {
140
        return $this->parent;
141
    }
142
143
    /**
144
     * @return TerminalInterface
145
     */
146
    public function getTerminal()
147
    {
148
        return $this->terminal;
149
    }
150
151
    /**
152
     * @return bool
153
     */
154
    public function isOpen()
155
    {
156
        return $this->open;
157
    }
158
159
    /**
160
     * Add a new Item to the listing
161
     *
162
     * @param MenuItemInterface $item
163
     */
164
    public function addItem(MenuItemInterface $item)
165
    {
166
        $this->items[] = $item;
167
    }
168
169
    /**
170
     * Set the selected pointer to the first selectable item
171
     */
172
    private function selectFirstItem()
173
    {
174
        foreach ($this->items as $key => $item) {
175
            if ($item->canSelect()) {
176
                $this->selectedItem = $key;
177
                break;
178
            }
179
        }
180
    }
181
182
    /**
183
     * Display menu and capture input
184
     */
185
    private function display()
186
    {
187
        $this->draw();
188
189
        while ($this->isOpen() && $input = $this->terminal->getKeyedInput()) {
190
            switch ($input) {
191
                case 'up':
192
                case 'down':
193
                    $this->moveSelection($input);
194
                    $this->draw();
195
                    break;
196
                case 'enter':
197
                    $this->executeCurrentItem();
198
                    break;
199
            }
200
        }
201
    }
202
203
    /**
204
     * Move the selection in a given direction, up / down
205
     *
206
     * @param $direction
207
     */
208
    protected function moveSelection($direction)
209
    {
210
        do {
211
            $itemKeys = array_keys($this->items);
212
213
            $direction === 'up'
214
                ? $this->selectedItem--
215
                : $this->selectedItem++;
216
217
            if (!array_key_exists($this->selectedItem, $this->items)) {
218
                $this->selectedItem  = $direction === 'up'
0 ignored issues
show
Documentation Bug introduced by
It seems like $direction === 'up' ? en...eys) : reset($itemKeys) can also be of type false. However, the property $selectedItem is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
219
                    ? end($itemKeys)
220
                    : reset($itemKeys);
221
            } elseif ($this->getSelectedItem()->canSelect()) {
222
                return;
223
            }
224
225
        } while (!$this->getSelectedItem()->canSelect());
226
    }
227
228
    /**
229
     * @return MenuItemInterface
230
     */
231
    public function getSelectedItem()
232
    {
233
        return $this->items[$this->selectedItem];
234
    }
235
236
    /**
237
     * Execute the current item
238
     */
239
    protected function executeCurrentItem()
240
    {
241
        $item = $this->getSelectedItem();
242
243
        if ($item->canSelect()) {
244
            $callable = $item->getSelectAction();
245
            $callable($this);
246
        }
247
    }
248
249
    /**
250
     * Draw the menu to STDOUT
251
     */
252
    protected function draw()
253
    {
254
        $this->terminal->clean();
255
        $this->terminal->moveCursorToTop();
256
257
        echo "\n\n";
258
259
        if (is_string($this->title)) {
260
            $this->drawMenuItem(new LineBreakItem());
261
            $this->drawMenuItem(new StaticItem($this->title));
262
            $this->drawMenuItem(new LineBreakItem($this->style->getTitleSeparator()));
263
        }
264
265
        array_map(function ($item, $index) {
266
            $this->drawMenuItem($item, $index === $this->selectedItem);
267
        }, $this->items, array_keys($this->items));
268
269
        $this->drawMenuItem(new LineBreakItem());
270
271
        echo "\n\n";
272
    }
273
274
    /**
275
     * Draw a menu item
276
     *
277
     * @param MenuItemInterface $item
278
     * @param bool|false $selected
279
     */
280
    protected function drawMenuItem(MenuItemInterface $item, $selected = false)
281
    {
282
        $rows = $item->getRows($this->style, $selected);
283
284
        $setColour = $selected
285
            ? $this->style->getSelectedSetCode()
286
            : $this->style->getUnselectedSetCode();
287
288
        $unsetColour = $selected
289
            ? $this->style->getSelectedUnsetCode()
290
            : $this->style->getUnselectedUnsetCode();
291
292
        foreach ($rows as $row) {
293
            echo sprintf(
294
                "%s%s%s%s%s%s%s",
295
                str_repeat(' ', $this->style->getMargin()),
296
                $setColour,
297
                str_repeat(' ', $this->style->getPadding()),
298
                $row,
299
                str_repeat(' ', $this->style->getRightHandPadding(mb_strlen($row))),
300
                $unsetColour,
301
                str_repeat(' ', $this->style->getMargin())
302
            );
303
304
            echo "\n\r";
305
        }
306
    }
307
308
    /**
309
     * @throws InvalidTerminalException
310
     */
311
    public function open()
312
    {
313
        if ($this->isOpen()) {
314
            return;
315
        }
316
317
        $this->configureTerminal();
318
        $this->open = true;
319
        $this->display();
320
    }
321
322
    /**
323
     * Close the menu
324
     *
325
     * @throws InvalidTerminalException
326
     */
327
    public function close()
328
    {
329
        $menu = $this;
330
331
        do {
332
            $menu->closeThis();
333
            $menu = $menu->getParent();
334
        } while (null !== $menu);
335
        
336
        $this->tearDownTerminal();
337
    }
338
339
    /**
340
     * @throws InvalidTerminalException
341
     */
342
    public function closeThis()
343
    {
344
        $this->terminal->clean();
345
        $this->terminal->moveCursorToTop();
346
        $this->open = false;
347
    }
348
}
349