Completed
Push — master ( 306131...5909ec )
by Aydin
06:33 queued 03:57
created

src/MenuItem/SplitItem.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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("\n", StringUtil::wordwrap(sprintf('%s%s', $marker, $item->getText()), $length)),
141
                    $length,
142
                    $style,
143
                    $isSelected,
144
                    $itemExtra
145
                );
146
            }, array_keys($this->items), $this->items),
147
            $style,
148
            $missingLength,
149
            $length
150
        );
151
    }
152
153
    private function buildRows(array $cells, MenuStyle $style, int $missingLength, int $length) : array
154
    {
155
        $extraPadLength = $style->getDisplaysExtra() ? 2 + mb_strlen($style->getItemExtra()) : 0;
156
        
157
        return array_map(
158
            function ($i) use ($cells, $length, $missingLength, $extraPadLength) {
159
                return $this->buildRow($cells, $i, $length, $missingLength, $extraPadLength);
160
            },
161
            range(0, max(array_map('count', $cells)) - 1)
162
        );
163
    }
164
165
    private function buildRow(array $cells, int $index, int $length, int $missingLength, int $extraPadLength) : string
166
    {
167
        return sprintf(
168
            '%s%s',
169
            implode(
170
                '',
171
                array_map(
172
                    function ($cell) use ($index, $length, $extraPadLength) {
173
                        return $cell[$index] ?? str_repeat(' ', $length + $this->gutter + $extraPadLength);
174
                    },
175
                    $cells
176
                )
177
            ),
178
            str_repeat(' ', $missingLength)
179
        );
180
    }
181
182
    private function buildCell(
183
        array $content,
184
        int $length,
185
        MenuStyle $style,
186
        bool $isSelected,
187
        string $itemExtra
188
    ) : array {
189
        return array_map(function ($row, $index) use ($length, $style, $isSelected, $itemExtra) {
190
            $invertedColoursSetCode = $isSelected
191
                ? $style->getInvertedColoursSetCode()
192
                : '';
193
            $invertedColoursUnsetCode = $isSelected
194
                ? $style->getInvertedColoursUnsetCode()
195
                : '';
196
197
            return sprintf(
198
                '%s%s%s%s%s%s',
199
                $invertedColoursSetCode,
200
                $row,
201
                str_repeat(' ', $length - mb_strlen($row)),
202
                $index === 0 ? $itemExtra : str_repeat(' ', mb_strlen($itemExtra)),
203
                $invertedColoursUnsetCode,
204
                str_repeat(' ', $this->gutter)
205
            );
206
        }, $content, array_keys($content));
207
    }
208
209
    /**
210
     * Is there an item with this index and can it be
211
     * selected?
212
     */
213
    public function canSelectIndex(int $index) : bool
214
    {
215
        return isset($this->items[$index]) && $this->items[$index]->canSelect();
216
    }
217
218
    /**
219
     * Set the item index which should be selected. If the item does
220
     * not exist then throw an exception.
221
     */
222
    public function setSelectedItemIndex(int $index) : void
223
    {
224
        if (!isset($this->items[$index])) {
225
            throw new \InvalidArgumentException(sprintf('Index: "%s" does not exist', $index));
226
        }
227
        
228
        $this->selectedItemIndex = $index;
229
    }
230
231
    /**
232
     * Get the currently select item index.
233
     * May be null in case of no selectable item.
234
     */
235
    public function getSelectedItemIndex() : ?int
236
    {
237
        return $this->selectedItemIndex;
238
    }
239
240
    /**
241
     * Get the currently selected item - if no items are selectable
242
     * then throw an exception.
243
     */
244
    public function getSelectedItem() : MenuItemInterface
245
    {
246
        if (null === $this->selectedItemIndex) {
247
            throw new \RuntimeException('No item is selected');
248
        }
249
        
250
        return $this->items[$this->selectedItemIndex];
251
    }
252
253
    public function getItems() : array
254
    {
255
        return $this->items;
256
    }
257
258
    /**
259
     * Can the item be selected
260
     * In this case, it indicates if at least 1 item inside the SplitItem can be selected
261
     */
262
    public function canSelect() : bool
263
    {
264
        return $this->canBeSelected;
265
    }
266
267
    /**
268
     * Execute the items callable if required
269
     */
270
    public function getSelectAction() : ?callable
271
    {
272
        return null;
273
    }
274
275
    /**
276
     * Whether or not the menu item is showing the menustyle extra value
277
     */
278
    public function showsItemExtra() : bool
279
    {
280
        return false;
281
    }
282
283
    /**
284
     * Enable showing item extra
285
     */
286
    public function showItemExtra() : void
287
    {
288
        //noop
289
    }
290
291
    /**
292
     * Disable showing item extra
293
     */
294
    public function hideItemExtra() : void
295
    {
296
        //noop
297
    }
298
299
    /**
300
     * Nothing to return with SplitItem
301
     */
302
    public function getText() : string
303
    {
304
        throw new \BadMethodCallException(sprintf('Not supported on: %s', __CLASS__));
305
    }
306
}
307