Completed
Push — master ( ec0418...578cbe )
by Aydin
06:03 queued 02:48
created

SplitItem   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 2
dl 0
loc 300
rs 8.2608
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setGutter() 0 5 1
A addItem() 0 11 3
A addItems() 0 8 2
A setItems() 0 6 1
A setDefaultSelectedItem() 0 13 3
B getRows() 0 54 8
A buildRows() 0 11 2
A buildRow() 0 16 1
B buildCell() 0 26 4
A canSelectIndex() 0 4 2
A setSelectedItemIndex() 0 8 2
A getSelectedItemIndex() 0 4 1
A getSelectedItem() 0 8 2
A getItems() 0 4 1
A canSelect() 0 4 1
A getSelectAction() 0 4 1
A showsItemExtra() 0 4 1
A showItemExtra() 0 4 1
A hideItemExtra() 0 4 1
A getText() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like SplitItem 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 SplitItem, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpSchool\CliMenu\MenuItem;
4
5
use Assert\Assertion;
6
use PhpSchool\CliMenu\CliMenu;
7
use PhpSchool\CliMenu\CliMenuBuilder;
8
use PhpSchool\CliMenu\MenuStyle;
9
use PhpSchool\CliMenu\Util\StringUtil;
10
11
/**
12
 * @author Michael Woodward <[email protected]>
13
 */
14
class SplitItem implements MenuItemInterface
15
{
16
    /**
17
     * @var array
18
     */
19
    private $items = [];
20
21
    /**
22
     * @var int|null
23
     */
24
    private $selectedItemIndex;
25
26
    /**
27
     * @var bool
28
     */
29
    private $canBeSelected = true;
30
31
    /**
32
     * @var int
33
     */
34
    private $gutter = 2;
35
36
    /**
37
     * @var array
38
     */
39
    private static $blacklistedItems = [
40
        \PhpSchool\CliMenu\MenuItem\AsciiArtItem::class,
41
        \PhpSchool\CliMenu\MenuItem\LineBreakItem::class,
42
        \PhpSchool\CliMenu\MenuItem\SplitItem::class,
43
    ];
44
45
    public function __construct(array $items = [])
46
    {
47
        $this->addItems($items);
48
        $this->setDefaultSelectedItem();
49
    }
50
51
    public function setGutter(int $gutter) : void
52
    {
53
        Assertion::greaterOrEqualThan($gutter, 0);
54
        $this->gutter = $gutter;
55
    }
56
57
    public function addItem(MenuItemInterface $item) : self
58
    {
59
        foreach (self::$blacklistedItems as $bl) {
60
            if ($item instanceof $bl) {
61
                throw new \InvalidArgumentException("Cannot add a $bl to a SplitItem");
62
            }
63
        }
64
        $this->items[] = $item;
65
        $this->setDefaultSelectedItem();
66
        return $this;
67
    }
68
69
    public function addItems(array $items) : self
70
    {
71
        foreach ($items as $item) {
72
            $this->addItem($item);
73
        }
74
            
75
        return $this;
76
    }
77
78
    public function setItems(array $items) : self
79
    {
80
        $this->items = [];
81
        $this->addItems($items);
82
        return $this;
83
    }
84
85
    /**
86
     * Select default item
87
     */
88
    private function setDefaultSelectedItem() : void
89
    {
90
        foreach ($this->items as $index => $item) {
91
            if ($item->canSelect()) {
92
                $this->canBeSelected = true;
93
                $this->selectedItemIndex = $index;
0 ignored issues
show
Documentation Bug introduced by
It seems like $index can also be of type string. However, the property $selectedItemIndex is declared as type integer|null. 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...
94
                return;
95
            }
96
        }
97
98
        $this->canBeSelected = false;
99
        $this->selectedItemIndex = null;
100
    }
101
102
    /**
103
     * The output text for the item
104
     */
105
    public function getRows(MenuStyle $style, bool $selected = false) : array
106
    {
107
        $numberOfItems = count($this->items);
108
109
        if ($numberOfItems === 0) {
110
            throw new \RuntimeException(sprintf('There should be at least one item added to: %s', __CLASS__));
111
        }
112
        
113
        if (!$selected) {
114
            $this->setDefaultSelectedItem();
115
        }
116
117
        $length = $style->getDisplaysExtra()
118
            ? floor($style->getContentWidth() / $numberOfItems) - (mb_strlen($style->getItemExtra()) + 2)
119
            : floor($style->getContentWidth() / $numberOfItems);
120
        
121
        $length -= $this->gutter;
122
        
123
        $missingLength = $style->getContentWidth() % $numberOfItems;
124
        
125
        return $this->buildRows(
126
            array_map(function ($index, $item) use ($selected, $length, $style) {
127
                $isSelected = $selected && $index === $this->selectedItemIndex;
128
                $marker = $item->canSelect()
129
                    ? sprintf('%s', $style->getMarker($isSelected))
130
                    : '';
131
132
                $itemExtra = '';
133
                if ($style->getDisplaysExtra()) {
134
                    $itemExtra = $item->showsItemExtra()
135
                        ? sprintf('  %s', $style->getItemExtra())
136
                        : sprintf('  %s', str_repeat(' ', mb_strlen($style->getItemExtra())));
137
                }
138
139
                return $this->buildCell(
140
                    explode(
141
                        "\n",
142
                        StringUtil::wordwrap(
143
                            sprintf('%s%s', $marker, $item->getText()),
144
                            $length,
145
                            sprintf("\n%s", str_repeat(' ', mb_strlen($marker)))
146
                        )
147
                    ),
148
                    $length,
149
                    $style,
150
                    $isSelected,
151
                    $itemExtra
152
                );
153
            }, array_keys($this->items), $this->items),
154
            $style,
155
            $missingLength,
156
            $length
157
        );
158
    }
159
160
    private function buildRows(array $cells, MenuStyle $style, int $missingLength, int $length) : array
161
    {
162
        $extraPadLength = $style->getDisplaysExtra() ? 2 + mb_strlen($style->getItemExtra()) : 0;
163
        
164
        return array_map(
165
            function ($i) use ($cells, $length, $missingLength, $extraPadLength) {
166
                return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength);
167
            },
168
            range(0, max(array_map('count', $cells)) - 1)
169
        );
170
    }
171
172
    private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string
173
    {
174
        return sprintf(
175
            '%s%s',
176
            implode(
177
                '',
178
                array_map(
179
                    function ($cell) use ($index, $length, $extraPadLength) {
180
                        return $cell[$index] ?? str_repeat(' ', $length + $this->gutter + $extraPadLength);
181
                    },
182
                    $cells
183
                )
184
            ),
185
            str_repeat(' ', $missingLength)
186
        );
187
    }
188
189
    private function buildCell(
190
        array $content,
191
        int $length,
192
        MenuStyle $style,
193
        bool $isSelected,
194
        string $itemExtra
195
    ) : array {
196
        return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) {
197
            $invertedColoursSetCode = $isSelected
198
                ? $style->getInvertedColoursSetCode()
199
                : '';
200
            $invertedColoursUnsetCode = $isSelected
201
                ? $style->getInvertedColoursUnsetCode()
202
                : '';
203
204
            return sprintf(
205
                '%s%s%s%s%s%s',
206
                $invertedColoursSetCode,
207
                $row,
208
                str_repeat(' ', $length - mb_strlen($row)),
209
                $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)),
210
                $invertedColoursUnsetCode,
211
                str_repeat(' ', $this->gutter)
212
            );
213
        }, $content, array_keys($content));
214
    }
215
216
    /**
217
     * Is there an item with this index and can it be
218
     * selected?
219
     */
220
    public function canSelectIndex(int $index) : bool
221
    {
222
        return isset($this->items[$index]) && $this->items[$index]->canSelect();
223
    }
224
225
    /**
226
     * Set the item index which should be selected. If the item does
227
     * not exist then throw an exception.
228
     */
229
    public function setSelectedItemIndex(int $index) : void
230
    {
231
        if (!isset($this->items[$index])) {
232
            throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index));
233
        }
234
        
235
        $this->selectedItemIndex = $index;
236
    }
237
238
    /**
239
     * Get the currently select item index.
240
     * May be null in case of no selectable item.
241
     */
242
    public function getSelectedItemIndex() : ?int
243
    {
244
        return $this->selectedItemIndex;
245
    }
246
247
    /**
248
     * Get the currently selected item - if no items are selectable
249
     * then throw an exception.
250
     */
251
    public function getSelectedItem() : MenuItemInterface
252
    {
253
        if (null === $this->selectedItemIndex) {
254
            throw new \RuntimeException('No item is selected');
255
        }
256
        
257
        return $this->items[$this->selectedItemIndex];
258
    }
259
260
    public function getItems() : array
261
    {
262
        return $this->items;
263
    }
264
265
    /**
266
     * Can the item be selected
267
     * In this case, it indicates if at least 1 item inside the SplitItem can be selected
268
     */
269
    public function canSelect() : bool
270
    {
271
        return $this->canBeSelected;
272
    }
273
274
    /**
275
     * Execute the items callable if required
276
     */
277
    public function getSelectAction() : ?callable
278
    {
279
        return null;
280
    }
281
282
    /**
283
     * Whether or not the menu item is showing the menustyle extra value
284
     */
285
    public function showsItemExtra() : bool
286
    {
287
        return false;
288
    }
289
290
    /**
291
     * Enable showing item extra
292
     */
293
    public function showItemExtra() : void
294
    {
295
        //noop
296
    }
297
298
    /**
299
     * Disable showing item extra
300
     */
301
    public function hideItemExtra() : void
302
    {
303
        //noop
304
    }
305
306
    /**
307
     * Nothing to return with SplitItem
308
     */
309
    public function getText() : string
310
    {
311
        throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__));
312
    }
313
}
314