Accordion::renderItem()   C
last analyzed

Complexity

Conditions 13
Paths 145

Size

Total Lines 83
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 13.0012

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 56
c 1
b 0
f 0
nc 145
nop 3
dl 0
loc 83
ccs 50
cts 51
cp 0.9804
crap 13.0012
rs 6.2416

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\Bootstrap4;
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 is_array;
17
use function is_int;
18
use function is_numeric;
19
use function is_object;
20
use function is_string;
21
22
/**
23
 * Accordion renders an accordion bootstrap javascript component.
24
 *
25
 * For example:
26
 *
27
 * ```php
28
 * echo Accordion::widget()
29
 *     ->items([
30
 *         // equivalent to the above
31
 *         [
32
 *             'label' => 'Collapsible Group Item #1',
33
 *             'content' => 'Anim pariatur cliche...',
34
 *             // open its content by default
35
 *             'contentOptions' => ['class' => 'in'],
36
 *         ],
37
 *         // another group item
38
 *         [
39
 *             'label' => 'Collapsible Group Item #1',
40
 *             'content' => 'Anim pariatur cliche...',
41
 *             'contentOptions' => [...],
42
 *             'options' => [...],
43
 *             'expand' => true,
44
 *         ],
45
 *         // if you want to swap out .card-block with .list-group, you may use the following
46
 *         [
47
 *             'label' => 'Collapsible Group Item #1',
48
 *             'content' => [
49
 *                 'Anim pariatur cliche...',
50
 *                 'Anim pariatur cliche...',
51
 *             ],
52
 *             'contentOptions' => [...],
53
 *             'options' => [...],
54
 *             'footer' => 'Footer' // the footer label in list-group,
55
 *         ],
56
 *     ]);
57
 * ```
58
 */
59
class Accordion extends Widget
60
{
61
    private array $items = [];
62
    private bool $encodeLabels = true;
63
    private bool $autoCloseItems = true;
64
    private array $itemToggleOptions = [];
65
    private array $options = [];
66
67 7
    protected function run(): string
68
    {
69 7
        if (!isset($this->options['id'])) {
70 7
            $this->options['id'] = "{$this->getId()}-accordion";
71
        }
72
73 7
        $this->registerPlugin('collapse', $this->options);
74
75 7
        Html::addCssClass($this->options, 'accordion');
76
77 7
        return implode("\n", [
78 7
            Html::beginTag('div', $this->options),
79 7
            $this->renderItems(),
80 4
            Html::endTag('div'),
81 4
        ]) . "\n";
82
    }
83
84
    /**
85
     * Renders collapsible items as specified on {@see items}.
86
     *
87
     * @throws RuntimeException|JsonException|
88
     *
89
     * @return string the rendering result.
90
     */
91 7
    public function renderItems(): string
92
    {
93 7
        $items = [];
94 7
        $index = 0;
95 7
        $expanded = array_search(true, array_column($this->items, 'expand'), true);
96
97 7
        foreach ($this->items as $key => $item) {
98 7
            if (!is_array($item)) {
99 1
                $item = ['content' => $item];
100
            }
101
102 7
            if ($expanded === false && $index === 0) {
103 6
                $item['expand'] = true;
104
            }
105
106 7
            if (!array_key_exists('label', $item)) {
107 3
                if (is_int($key)) {
108 3
                    throw new RuntimeException('The "label" option is required.');
109
                }
110
111
                $item['label'] = $key;
112
            }
113
114 4
            $header = ArrayHelper::remove($item, 'label');
115 4
            $options = ArrayHelper::getValue($item, 'options', []);
116
117 4
            Html::addCssClass($options, ['panel' => 'card']);
118
119 4
            $items[] = Html::tag('div', $this->renderItem($header, $item, $index++), $options);
0 ignored issues
show
Bug introduced by
It seems like $header can also be of type null; however, parameter $header of Yiisoft\Yii\Bootstrap4\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

119
            $items[] = Html::tag('div', $this->renderItem(/** @scrutinizer ignore-type */ $header, $item, $index++), $options);
Loading history...
Bug introduced by
It seems like $item can also be of type object; however, parameter $item of Yiisoft\Yii\Bootstrap4\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

119
            $items[] = Html::tag('div', $this->renderItem($header, /** @scrutinizer ignore-type */ $item, $index++), $options);
Loading history...
120
        }
121
122 4
        return implode("\n", $items);
123
    }
124
125
    /**
126
     * Renders a single collapsible item group.
127
     *
128
     * @param string $header a label of the item group {@see items}
129
     * @param array $item a single item from {@see items}
130
     * @param int $index the item index as each item group content must have an id.
131
     *
132
     * @throws JsonException|RuntimeException
133
     *
134
     * @return string the rendering result
135
     */
136 4
    public function renderItem(string $header, array $item, int $index): string
137
    {
138 4
        if (array_key_exists('content', $item)) {
139 4
            $id = $this->options['id'] . '-collapse' . $index;
140 4
            $expand = ArrayHelper::remove($item, 'expand', false);
141 4
            $options = ArrayHelper::getValue($item, 'contentOptions', []);
142 4
            $options['id'] = $id;
143
144 4
            Html::addCssClass($options, ['widget' => 'collapse']);
145
146 4
            if ($expand) {
147 4
                Html::addCssClass($options, 'show');
148
            }
149
150 4
            if (!isset($options['aria-label'], $options['aria-labelledby'])) {
151 4
                $options['aria-labelledby'] = $options['id'] . '-heading';
152
            }
153
154 4
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
155
156 4
            if ($encodeLabel) {
157 4
                $header = Html::encode($header);
158
            }
159
160 4
            $itemToggleOptions = array_merge([
161 4
                'tag' => 'button',
162 4
                'type' => 'button',
163 4
                'data-toggle' => 'collapse',
164 4
                'data-target' => '#' . $options['id'],
165 4
                'aria-expanded' => $expand ? 'true' : 'false',
166 4
                'aria-controls' => $options['id'],
167 4
            ], $this->itemToggleOptions);
168 4
            $itemToggleTag = ArrayHelper::remove($itemToggleOptions, 'tag', 'button');
169
170
            /** @psalm-suppress ConflictingReferenceConstraint */
171 4
            if ($itemToggleTag === 'a') {
172 1
                ArrayHelper::remove($itemToggleOptions, 'data-target');
173 1
                $headerToggle = Html::a($header, '#' . $id, $itemToggleOptions) . "\n";
174
            } else {
175 3
                Html::addCssClass($itemToggleOptions, 'btn-link');
176 3
                $headerToggle = Button::widget()
177 3
                    ->label($header)
0 ignored issues
show
Bug introduced by
The method label() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bootstrap4\Progress or Yiisoft\Yii\Bootstrap4\Button or Yiisoft\Yii\Bootstrap4\Nav or Yiisoft\Yii\Bootstrap4\ButtonDropdown. ( Ignorable by Annotation )

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

177
                    ->/** @scrutinizer ignore-call */ label($header)
Loading history...
178 3
                    ->encodeLabels(false)
179 3
                    ->options($itemToggleOptions)
180 3
                    ->render() . "\n";
181
            }
182
183 4
            $header = Html::tag('h5', $headerToggle, ['class' => 'mb-0']);
184
185 4
            if (is_string($item['content']) || is_numeric($item['content']) || is_object($item['content'])) {
186 4
                $content = Html::tag('div', $item['content'], ['class' => 'card-body']) . "\n";
187 1
            } elseif (is_array($item['content'])) {
188 1
                $content = Html::ul($item['content'], [
189 1
                    'class' => 'list-group',
190
                    'itemOptions' => [
191
                        'class' => 'list-group-item',
192
                    ],
193
                    'encode' => false,
194 1
                ]) . "\n";
195
            } else {
196 4
                throw new RuntimeException('The "content" option should be a string, array or object.');
197
            }
198
        } else {
199
            throw new RuntimeException('The "content" option is required.');
200
        }
201
202 4
        $group = [];
203
204 4
        if ($this->autoCloseItems) {
205 4
            $options['data-parent'] = '#' . $this->options['id'];
206
        }
207
208 4
        $group[] = Html::tag('div', $header, ['class' => 'card-header', 'id' => $options['id'] . '-heading']);
209 4
        $group[] = Html::beginTag('div', $options);
210 4
        $group[] = $content;
211
212 4
        if (isset($item['footer'])) {
213 1
            $group[] = Html::tag('div', $item['footer'], ['class' => 'card-footer']);
214
        }
215
216 4
        $group[] = Html::endTag('div');
217
218 4
        return implode("\n", $group);
219
    }
220
221
    /**
222
     * Whether to close other items if an item is opened. Defaults to `true` which causes an accordion effect.
223
     *
224
     * Set this to `false` to allow keeping multiple items open at once.
225
     *
226
     * @param bool $value
227
     *
228
     * @return $this
229
     */
230 1
    public function autoCloseItems(bool $value): self
231
    {
232 1
        $this->autoCloseItems = $value;
233
234 1
        return $this;
235
    }
236
237
    /**
238
     * Whether the labels for header items should be HTML-encoded.
239
     *
240
     * @param bool $value
241
     *
242
     * @return $this
243
     */
244
    public function encodeLabels(bool $value): self
245
    {
246
        $this->encodeLabels = $value;
247
248
        return $this;
249
    }
250
251
    /**
252
     * List of groups in the collapse widget. Each array element represents a single group with the following structure:
253
     *
254
     * - label: string, required, the group header label.
255
     * - encode: bool, optional, whether this label should be HTML-encoded. This param will override global
256
     *   `$this->encodeLabels` param.
257
     * - content: array|string|object, required, the content (HTML) of the group
258
     * - options: array, optional, the HTML attributes of the group
259
     * - contentOptions: optional, the HTML attributes of the group's content
260
     *
261
     * You may also specify this property as key-value pairs, where the key refers to the `label` and the value refers
262
     * to `content`. If value is a string it is interpreted as label. If it is an array, it is interpreted as explained
263
     * above.
264
     *
265
     * For example:
266
     *
267
     * ```php
268
     * echo Accordion::widget([
269
     *     'items' => [
270
     *       'Introduction' => 'This is the first collapsible menu',
271
     *       'Second panel' => [
272
     *           'content' => 'This is the second collapsible menu',
273
     *       ],
274
     *       [
275
     *           'label' => 'Third panel',
276
     *           'content' => 'This is the third collapsible menu',
277
     *       ],
278
     *   ]
279
     * ])
280
     * ```
281
     *
282
     * @param array $value
283
     *
284
     * @return $this
285
     */
286 7
    public function items(array $value): self
287
    {
288 7
        $this->items = $value;
289
290 7
        return $this;
291
    }
292
293
    /**
294
     * The HTML options for the item toggle tag. Key 'tag' might be used here for the tag name specification.
295
     *
296
     * For example:
297
     *
298
     * ```php
299
     * [
300
     *     'tag' => 'div',
301
     *     'class' => 'custom-toggle',
302
     * ]
303
     * ```
304
     *
305
     * @param array $value
306
     *
307
     * @return $this
308
     */
309 1
    public function itemToggleOptions(array $value): self
310
    {
311 1
        $this->itemToggleOptions = $value;
312
313 1
        return $this;
314
    }
315
316
    /**
317
     * The HTML attributes for the widget container tag. The following special options are recognized.
318
     *
319
     * @param array $value
320
     *
321
     * @return $this
322
     *
323
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
324
     */
325
    public function options(array $value): self
326
    {
327
        $this->options = $value;
328
329
        return $this;
330
    }
331
}
332