Completed
Pull Request — master (#194)
by
unknown
02:23
created

SplitItem   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 317
Duplicated Lines 0 %

Importance

Changes 17
Bugs 0 Features 1
Metric Value
eloc 123
dl 0
loc 317
rs 8.8
c 17
b 0
f 1
wmc 45

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setGutter() 0 4 1
A __construct() 0 4 1
A getGutter() 0 3 1
A setDefaultSelectedItem() 0 12 3
A setItems() 0 5 1
A addItem() 0 10 3
A addItems() 0 7 2
A getSelectAction() 0 3 1
A buildRow() 0 14 1
A getSelectedItemIndex() 0 3 1
A getSelectedItem() 0 7 2
A canSelectIndex() 0 3 2
A getText() 0 3 1
A canSelect() 0 3 1
C getRows() 0 66 12
A showItemExtra() 0 2 1
A setSelectedItemIndex() 0 7 2
A buildRows() 0 9 2
A hideItemExtra() 0 2 1
A showsItemExtra() 0 3 1
A buildCell() 0 25 4
A getItems() 0 3 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.

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