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