Passed
Pull Request — master (#35)
by Wilmer
02:14
created

Accordion::renderItem()   F

Complexity

Conditions 16
Paths 625

Size

Total Lines 86
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 16

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 16
eloc 54
c 2
b 0
f 0
nc 625
nop 3
dl 0
loc 86
ccs 52
cts 52
cp 1
crap 16
rs 1.9208

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function array_column;
13
use function array_key_exists;
14
use function array_merge;
15
use function array_search;
16
use function implode;
17
use function is_array;
18
use function is_int;
19
use function is_numeric;
20
use function is_object;
21
use function is_string;
22
23
/**
24
 * Accordion renders an accordion bootstrap javascript component.
25
 *
26
 * For example:
27
 *
28
 * ```php
29
 * echo Accordion::widget()
30
 *     ->witItems([
31
 *         [
32
 *             'label' => 'Collapsible Group Item #1',
33
 *             'content' => 'Anim pariatur cliche...',
34
 *             // open its content by default
35
 *             'contentOptions' => ['class' => 'show'],
36
 *         ],
37
 *         // another group item
38
 *         [
39
 *             'label' => 'Collapsible Group Item #2',
40
 *             'content' => 'Anim pariatur cliche...',
41
 *             'contentOptions' => [...],
42
 *             'options' => [...],
43
 *             'expand' => true,
44
 *         ],
45
 *         // if you want to swap out .accordion-body with .list-group, you may provide an array
46
 *         [
47
 *             'label' => 'Collapsible Group Item #3',
48
 *             'content' => [
49
 *                 'Anim pariatur cliche...',
50
 *                 'Anim pariatur cliche...',
51
 *             ],
52
 *             'contentOptions' => [...],
53
 *             'options' => [...],
54
 *         ],
55
 *     ]);
56
 * ```
57
 */
58
final class Accordion extends Widget
59
{
60
    private array $items = [];
61
    private bool $encodeLabels = true;
62
    private bool $encodeTags = false;
63
    private bool $autoCloseItems = true;
64
    private array $itemToggleOptions = [];
65
    private array $options = [];
66
67 11
    public function run(): string
68
    {
69 11
        if (!isset($this->options['id'])) {
70 11
            $this->options['id'] = "{$this->getId()}-accordion";
71
        }
72
73
        /** @psalm-suppress InvalidArgument */
74 11
        Html::addCssClass($this->options, 'accordion');
75
76 11
        if ($this->encodeTags === false) {
77 10
            $this->options = array_merge($this->options, ['encode' => false]);
78
        }
79
80 11
        return Html::div($this->renderItems(), $this->options);
81
    }
82
83
    /**
84
     * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect.
85
     *
86
     * Set this to `false` to allow keeping multiple items open at once.
87
     *
88
     * @param bool $value
89
     *
90
     * @return $this
91
     */
92 1
    public function withoutAutoCloseItems(bool $value = false): self
93
    {
94 1
        $new = clone $this;
95 1
        $new->autoCloseItems = $value;
96
97 1
        return $new;
98
    }
99
100
    /**
101
     * Whether the labels for header items should be HTML-encoded.
102
     *
103
     * @param bool $value
104
     *
105
     * @return $this
106
     */
107 1
    public function withoutEncodeLabels(bool $value = false): self
108
    {
109 1
        $new = clone $this;
110 1
        $new->encodeLabels = $value;
111
112 1
        return $new;
113
    }
114
115
    /**
116
     * List of groups in the collapse widget. Each array element represents a single group with the following structure:
117
     *
118
     * - label: string, required, the group header label.
119
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global
120
     *   `$this->encodeLabels` param.
121
     * - content: array|string|object, required, the content (HTML) of the group
122
     * - options: array, optional, the HTML attributes of the group
123
     * - contentOptions: optional, the HTML attributes of the group's content
124
     *
125
     * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers
126
     * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained
127
     * above.
128
     *
129
     * For example:
130
     *
131
     * ```php
132
     * echo Accordion::widget([
133
     *     'withItems' => [
134
     *       'Introduction' => 'This is the first collapsible menu',
135
     *       'Second panel' => [
136
     *           'content' => 'This is the second collapsible menu',
137
     *       ],
138
     *       [
139
     *           'label' => 'Third panel',
140
     *           'content' => 'This is the third collapsible menu',
141
     *       ],
142
     *   ]
143
     * ])
144
     * ```
145
     *
146
     * @param array $value
147
     *
148
     * @return $this
149
     */
150 11
    public function withItems(array $value): self
151
    {
152 11
        $new = clone $this;
153 11
        $new->items = $value;
154
155 11
        return $new;
156
    }
157
158
    /**
159
     * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
160
     *
161
     * For example:
162
     *
163
     * ```php
164
     * [
165
     *     'tag' => 'div',
166
     *     'class' => 'custom-toggle',
167
     * ]
168
     * ```
169
     *
170
     * @param array $value
171
     *
172
     * @return $this
173
     */
174 1
    public function withItemToggleOptions(array $value): self
175
    {
176 1
        $new = clone $this;
177 1
        $new->itemToggleOptions = $value;
178
179 1
        return $new;
180
    }
181
182
    /**
183
     * The HTML attributes for the widget container tag. The following special options are recognized.
184
     *
185
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
186
     *
187
     * @param array $value
188
     *
189
     * @return $this
190
     */
191 1
    public function withOptions(array $value): self
192
    {
193 1
        $new = clone $this;
194 1
        $new->options = $value;
195
196 1
        return $new;
197
    }
198
199
    /**
200
     * Allows you to enable or disable the encoding tags html.
201
     *
202
     * @param bool $value
203
     *
204
     * @return self
205
     */
206 1
    public function withEncodeTags(bool $value = true): self
207
    {
208 1
        $new = clone $this;
209 1
        $new->encodeTags = $value;
210
211 1
        return $new;
212
    }
213
214
    /**
215
     * Renders collapsible items as specified on {@see items}.
216
     *
217
     * @throws JsonException|RuntimeException
218
     *
219
     * @return string the rendering result
220
     */
221 11
    private function renderItems(): string
222
    {
223 11
        $items = [];
224 11
        $index = 0;
225 11
        $expanded = array_search(true, array_column($this->items, 'expand'), true);
226
227 11
        foreach ($this->items as $key => $item) {
228 11
            if (!is_array($item)) {
229 1
                $item = ['content' => $item];
230
            }
231
232 11
            if ($expanded === false && $index === 0) {
233 10
                $item['expand'] = true;
234
            }
235
236 11
            if (!array_key_exists('label', $item)) {
237 3
                throw new RuntimeException('The "label" option is required.');
238
            }
239
240 8
            $header = ArrayHelper::remove($item, 'label');
241 8
            $options = ArrayHelper::getValue($item, 'options', []);
242
243 8
            if ($this->encodeTags === false) {
244 7
                $options = array_merge($options, ['encode' => false]);
245
            }
246
247 8
            Html::addCssClass($options, ['panel' => 'accordion-item']);
248
249 8
            $items[] = Html::div($this->renderItem($header, $item, $index++), $options);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type object; however, parameter $item of Yiisoft\Yii\Bootstrap5\Accordion::renderItem() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
            $items[] = Html::div($this->renderItem($header, /** @scrutinizer ignore-type */ $item, $index++), $options);
Loading history...
Bug introduced by
It seems like $header can also be of type null; however, parameter $header of Yiisoft\Yii\Bootstrap5\Accordion::renderItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
            $items[] = Html::div($this->renderItem(/** @scrutinizer ignore-type */ $header, $item, $index++), $options);
Loading history...
250
        }
251
252 7
        return implode("\n", $items);
253
    }
254
255
    /**
256
     * Renders a single collapsible item group.
257
     *
258
     * @param string $header a label of the item group {@see items}
259
     * @param array $item a single item from {@see items}
260
     * @param int $index the item index as each item group content must have an id
261
     *
262
     * @throws JsonException|RuntimeException
263
     *
264
     * @return string the rendering result
265
     */
266 8
    private function renderItem(string $header, array $item, int $index): string
267
    {
268 8
        if (array_key_exists('content', $item)) {
269 7
            $id = $this->options['id'] . '-collapse' . $index;
270 7
            $expand = ArrayHelper::remove($item, 'expand', false);
271 7
            $options = ArrayHelper::getValue($item, 'contentOptions', []);
272 7
            $options['id'] = $id;
273
274 7
            Html::addCssClass($options, ['accordion-body', 'collapse']);
275
276 7
            if ($expand) {
277 7
                Html::addCssClass($options, 'show');
278
            }
279
280 7
            if (!isset($options['aria-label'], $options['aria-labelledby'])) {
281 7
                $options['aria-labelledby'] = $options['id'] . '-heading';
282
            }
283
284 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
285
286 7
            if ($encodeLabel) {
287 7
                $header = Html::encode($header);
288
            }
289
290 7
            $itemToggleOptions = array_merge([
291 7
                'tag' => 'button',
292 7
                'type' => 'button',
293 7
                'data-bs-toggle' => 'collapse',
294 7
                'data-bs-target' => '#' . $options['id'],
295 7
                'aria-expanded' => $expand ? 'true' : 'false',
296 7
            ], $this->itemToggleOptions);
297
298 7
            if ($this->encodeTags === false) {
299 6
                $itemToggleOptions = array_merge($itemToggleOptions, ['encode' => false]);
300
            }
301
302 7
            $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
303
304
            /** @psalm-suppress ConflictingReferenceConstraint */
305 7
            if ($itemToggleTag === 'a') {
306 1
                ArrayHelper::remove($itemToggleOptions, 'data-bs-target');
307 1
                $header = Html::a($header, '#' . $id, $itemToggleOptions) . "\n";
308
            } else {
309 6
                Html::addCssClass($itemToggleOptions, 'accordion-button');
310 6
                if (!$expand) {
311 6
                    Html::addCssClass($itemToggleOptions, 'collapsed');
312
                }
313 6
                $header = Html::button($header, $itemToggleOptions);
314
            }
315
316 7
            if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
317 7
                $content = $item['content'];
318 1
            } elseif (is_array($item['content'])) {
319 1
                $ulOptions = ['class' => 'list-group'];
320 1
                $ulItemOptions = ['itemOptions' => ['class' => 'list-group-item']];
321
322 1
                if ($this->encodeTags === false) {
323 1
                    $ulOptions = array_merge($ulOptions, ['encode' => false]);
324 1
                    $ulItemOptions['itemOptions'] = array_merge($ulItemOptions['itemOptions'], ['encode' => false]);
325
                }
326
327 1
                $content = Html::ul($item['content'], array_merge($ulOptions, $ulItemOptions)) . "\n";
328
            } else {
329 7
                throw new RuntimeException('The "content" option should be a string, array or object.');
330
            }
331
        } else {
332 1
            throw new RuntimeException('The "content" option is required.');
333
        }
334
335 7
        $group = [];
336
337 7
        if ($this->autoCloseItems) {
338 7
            $options['data-bs-parent'] = '#' . $this->options['id'];
339
        }
340
341 7
        $groupOptions = ['class' => 'accordion-header', 'id' => $options['id'] . '-heading'];
342
343 7
        if ($this->encodeTags === false) {
344 6
            $options = array_merge($options, ['encode' => false]);
345 6
            $groupOptions = array_merge($groupOptions, ['encode' => false]);
346
        }
347
348 7
        $group[] = Html::tag('h2', $header, $groupOptions);
349 7
        $group[] = Html::div($content, $options);
350
351 7
        return implode("\n", $group);
352
    }
353
}
354