Dropdown::renderItem()   B
last analyzed

Complexity

Conditions 10
Paths 33

Size

Total Lines 47
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 30
c 0
b 0
f 0
nc 33
nop 1
dl 0
loc 47
rs 7.6666

How to fix   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\Widgets;
6
7
use InvalidArgumentException;
8
use Yiisoft\Definitions\Exception\CircularReferenceException;
9
use Yiisoft\Definitions\Exception\InvalidConfigException;
10
use Yiisoft\Definitions\Exception\NotInstantiableException;
11
use Yiisoft\Factory\NotFoundException;
12
use Yiisoft\Html\Html;
13
use Yiisoft\Html\Tag\A;
14
use Yiisoft\Html\Tag\Button;
15
use Yiisoft\Html\Tag\Span;
16
use Yiisoft\Widget\Widget;
17
18
use function array_merge;
19
use function gettype;
20
use function implode;
21
use function str_contains;
22
use function trim;
23
24
final class Dropdown extends Widget
25
{
26
    private string $activeClass = 'active';
27
    private bool $container = true;
28
    private array $containerAttributes = [];
29
    private string $containerClass = '';
30
    private string $containerTag = 'div';
31
    private string $disabledClass = 'disabled';
32
    private array $dividerAttributes = [];
33
    private string $dividerClass = 'dropdown-divider';
34
    private string $dividerTag = 'hr';
35
    private string $headerClass = '';
36
    private string $headerTag = 'span';
37
    private string $id = '';
38
    private string $itemClass = '';
39
    private string $itemTag = 'a';
40
    private bool $itemContainer = true;
41
    private array $itemContainerAttributes = [];
42
    private string $itemContainerTag = 'li';
43
    private array $items = [];
44
    private array $itemsContainerAttributes = [];
45
    private string $itemsContainerTag = 'ul';
46
    private array $splitButtonAttributes = [];
47
    private array $splitButtonSpanAttributes = [];
48
    private array $toggleAttributes = [];
49
    private string $toggleType = 'button';
50
51
    /**
52
     * Returns a new instance with the specified active class.
53
     *
54
     * @param string $value The active class.
55
     */
56
    public function activeClass(string $value): self
57
    {
58
        $new = clone $this;
59
        $new->activeClass = $value;
60
61
        return $new;
62
    }
63
64
    /**
65
     * Returns a new instance with the specified if the container is enabled, or not. Default is true.
66
     *
67
     * @param bool $value The container enabled.
68
     */
69
    public function container(bool $value): self
70
    {
71
        $new = clone $this;
72
        $new->container = $value;
73
74
        return $new;
75
    }
76
77
    /**
78
     * Returns a new instance with the specified container HTML attributes.
79
     *
80
     * @param array $valuesMap Attribute values indexed by attribute names.
81
     */
82
    public function containerAttributes(array $valuesMap): self
83
    {
84
        $new = clone $this;
85
        $new->containerAttributes = $valuesMap;
86
87
        return $new;
88
    }
89
90
    /**
91
     * Returns a new instance with the specified container class.
92
     *
93
     * @param string $value The container class.
94
     */
95
    public function containerClass(string $value): self
96
    {
97
        $new = clone $this;
98
        $new->containerClass = $value;
99
100
        return $new;
101
    }
102
103
    /**
104
     * Returns a new instance with the specified container tag.
105
     *
106
     * @param string $value The container tag.
107
     */
108
    public function containerTag(string $value): self
109
    {
110
        $new = clone $this;
111
        $new->containerTag = $value;
112
        return $new;
113
    }
114
115
    /**
116
     * Returns a new instance with the specified disabled class.
117
     *
118
     * @param string $value The disabled class.
119
     */
120
    public function disabledClass(string $value): self
121
    {
122
        $new = clone $this;
123
        $new->disabledClass = $value;
124
125
        return $new;
126
    }
127
128
    /**
129
     * Returns a new instance with the specified divider HTML attributes.
130
     *
131
     * @param array $valuesMap Attribute values indexed by attribute names.
132
     */
133
    public function dividerAttributes(array $valuesMap): self
134
    {
135
        $new = clone $this;
136
        $new->dividerAttributes = $valuesMap;
137
138
        return $new;
139
    }
140
141
    /**
142
     * Returns a new instance with the specified divider class.
143
     *
144
     * @param string $value The divider class.
145
     */
146
    public function dividerClass(string $value): self
147
    {
148
        $new = clone $this;
149
        $new->dividerClass = $value;
150
151
        return $new;
152
    }
153
154
    /**
155
     * Returns a new instance with the specified divider tag.
156
     *
157
     * @param string $value The divider tag.
158
     */
159
    public function dividerTag(string $value): self
160
    {
161
        $new = clone $this;
162
        $new->dividerTag = $value;
163
164
        return $new;
165
    }
166
167
    /**
168
     * Returns a new instance with the specified header class.
169
     *
170
     * @param string $value The header class.
171
     */
172
    public function headerClass(string $value): self
173
    {
174
        $new = clone $this;
175
        $new->headerClass = $value;
176
177
        return $new;
178
    }
179
180
    /**
181
     * Returns a new instance with the specified header tag.
182
     *
183
     * @param string $value The header tag.
184
     */
185
    public function headerTag(string $value): self
186
    {
187
        $new = clone $this;
188
        $new->headerTag = $value;
189
190
        return $new;
191
    }
192
193
    /**
194
     * Returns a new instance with the specified Widget ID.
195
     *
196
     * @param string $value The id of the widget.
197
     */
198
    public function id(string $value): self
199
    {
200
        $new = clone $this;
201
        $new->id = $value;
202
203
        return $new;
204
    }
205
206
    /**
207
     * Returns a new instance with the specified item class.
208
     *
209
     * @param string $value The item class.
210
     */
211
    public function itemClass(string $value): self
212
    {
213
        $new = clone $this;
214
        $new->itemClass = $value;
215
216
        return $new;
217
    }
218
219
    /**
220
     * Returns a new instance with the specified item container, if false, the item container will not be rendered.
221
     *
222
     * @param bool $value The item container.
223
     */
224
    public function itemContainer(bool $value): self
225
    {
226
        $new = clone $this;
227
        $new->itemContainer = $value;
228
229
        return $new;
230
    }
231
232
    /**
233
     * Returns a new instance with the specified item container HTML attributes.
234
     *
235
     * @param array $valuesMap Attribute values indexed by attribute names.
236
     */
237
    public function itemContainerAttributes(array $valuesMap): self
238
    {
239
        $new = clone $this;
240
        $new->itemContainerAttributes = $valuesMap;
241
242
        return $new;
243
    }
244
245
    /**
246
     * Returns a new instance with the specified item container class.
247
     *
248
     * @param string $value The item container class.
249
     */
250
    public function itemContainerClass(string $value): self
251
    {
252
        $new = clone $this;
253
        Html::addCssClass($new->itemContainerAttributes, $value);
254
255
        return $new;
256
    }
257
258
    /**
259
     * Returns a new instance with the specified item container tag.
260
     *
261
     * @param string $value The item container tag.
262
     */
263
    public function itemContainerTag(string $value): self
264
    {
265
        $new = clone $this;
266
        $new->itemContainerTag = $value;
267
268
        return $new;
269
    }
270
271
    /**
272
     * Returns a new instance with the specified item tag.
273
     *
274
     * @param string $value The item tag.
275
     */
276
    public function itemTag(string $value): self
277
    {
278
        $new = clone $this;
279
        $new->itemTag = $value;
280
281
        return $new;
282
    }
283
284
    /**
285
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
286
     * single menu with the following structure:
287
     *
288
     * - label: string, required, the nav item label.
289
     * - active: bool, whether the item should be on active state or not.
290
     * - disabled: bool, whether the item should be on disabled state or not. For default `disabled` is false.
291
     * - enclose: bool, whether the item should be enclosed by a `<li>` tag or not. For default `enclose` is true.
292
     * - encode: bool, whether the label should be HTML encoded or not. For default `encodeLabel` is true.
293
     * - headerAttributes: array, HTML attributes to be rendered in the item header.
294
     * - link: string, the item's href. Defaults to "#". For default `link` is "#".
295
     * - linkAttributes: array, the HTML attributes of the item's link. For default `linkAttributes` is `[]`.
296
     * - icon: string, the item's icon. For default `icon` is ``.
297
     * - iconAttributes: array, the HTML attributes of the item's icon. For default `iconAttributes` is `[]`.
298
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
299
     * - items: array, optional, the submenu items. The structure is the same as this property.
300
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
301
     * - itemsContainerAttributes: array, optional, the HTML attributes for tag `<li>`.
302
     *
303
     * To insert dropdown divider use `-`.
304
     */
305
    public function items(array $value): self
306
    {
307
        $new = clone $this;
308
        $new->items = $value;
309
310
        return $new;
311
    }
312
313
    /**
314
     * Returns a new instance with the specified items' container HTML attributes.
315
     *
316
     * @param array $valuesMap Attribute values indexed by attribute names.
317
     */
318
    public function itemsContainerAttributes(array $valuesMap): self
319
    {
320
        $new = clone $this;
321
        $new->itemsContainerAttributes = $valuesMap;
322
323
        return $new;
324
    }
325
326
    /**
327
     * Returns a new instance with the specified item container class.
328
     *
329
     * @param string $value The item container class.
330
     */
331
    public function itemsContainerClass(string $value): self
332
    {
333
        $new = clone $this;
334
        Html::addCssClass($new->itemsContainerAttributes, $value);
335
336
        return $new;
337
    }
338
339
    /**
340
     * Returns a new instance with the specified items' container tag.
341
     *
342
     * @param string $value The items' container tag.
343
     */
344
    public function itemsContainerTag(string $value): self
345
    {
346
        $new = clone $this;
347
        $new->itemsContainerTag = $value;
348
349
        return $new;
350
    }
351
352
    /**
353
     * Returns a new instance with the specified split button attributes.
354
     *
355
     * @param array $valuesMap Attribute values indexed by attribute names.
356
     */
357
    public function splitButtonAttributes(array $valuesMap): self
358
    {
359
        $new = clone $this;
360
        $new->splitButtonAttributes = $valuesMap;
361
362
        return $new;
363
    }
364
365
    /**
366
     * Returns a new instance with the specified split button class.
367
     *
368
     * @param string $value The split button class.
369
     */
370
    public function splitButtonClass(string $value): self
371
    {
372
        $new = clone $this;
373
        Html::addCssClass($new->splitButtonAttributes, $value);
374
375
        return $new;
376
    }
377
378
    /**
379
     * Returns a new instance with the specified split button span class.
380
     *
381
     * @param string $value The split button span class.
382
     */
383
    public function splitButtonSpanClass(string $value): self
384
    {
385
        $new = clone $this;
386
        Html::addCssClass($new->splitButtonSpanAttributes, $value);
387
388
        return $new;
389
    }
390
391
    /**
392
     * Returns a new instance with the specified toggle HTML attributes.
393
     *
394
     * @param array $valuesMap Attribute values indexed by attribute names.
395
     */
396
    public function toggleAttributes(array $valuesMap): self
397
    {
398
        $new = clone $this;
399
        $new->toggleAttributes = $valuesMap;
400
401
        return $new;
402
    }
403
404
    /**
405
     * Returns a new instance with the specified toggle class.
406
     *
407
     * @param string $value The toggle class.
408
     */
409
    public function toggleClass(string $value): self
410
    {
411
        $new = clone $this;
412
        Html::addCssClass($new->toggleAttributes, $value);
413
414
        return $new;
415
    }
416
417
    /**
418
     * Returns a new instance with the specified toggle type, if `button` the toggle will be a button, otherwise a
419
     * `a` tag will be used.
420
     *
421
     * @param string $value The toggle tag.
422
     */
423
    public function toggleType(string $value): self
424
    {
425
        $new = clone $this;
426
        $new->toggleType = $value;
427
428
        return $new;
429
    }
430
431
    /**
432
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
433
     */
434
    public function render(): string
435
    {
436
        /**
437
         * @psalm-var array<
438
         *   array-key,
439
         *   array{
440
         *     label: string,
441
         *     link: string,
442
         *     linkAttributes: array,
443
         *     active: bool,
444
         *     disabled: bool,
445
         *     enclose: bool,
446
         *     headerAttributes: array,
447
         *     itemContainerAttributes: array,
448
         *     toggleAttributes: array,
449
         *     visible: bool,
450
         *     items: array,
451
         *   }|string
452
         * > $normalizedItems
453
         */
454
        $normalizedItems = Helper\Normalizer::dropdown($this->items);
455
456
        $containerAttributes = $this->containerAttributes;
457
458
        $items = $this->renderItems($normalizedItems) . PHP_EOL;
459
460
        if (trim($items) === '') {
461
            return '';
462
        }
463
464
        if ($this->containerClass !== '') {
465
            Html::addCssClass($containerAttributes, $this->containerClass);
466
        }
467
468
        if ($this->containerTag === '') {
469
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
470
        }
471
472
        return match ($this->container) {
473
            true => Html::normalTag($this->containerTag, $items, $containerAttributes)->encode(false)->render(),
474
            false => $items,
475
        };
476
    }
477
478
    private function renderDivider(): string
479
    {
480
        $dividerAttributes = $this->dividerAttributes;
481
482
        if ($this->dividerClass !== '') {
483
            Html::addCssClass($dividerAttributes, $this->dividerClass);
484
        }
485
486
        if ($this->dividerTag === '') {
487
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
488
        }
489
490
        return $this->renderItemContainer(
491
            Html::tag($this->dividerTag, '', $dividerAttributes)->encode(false)->render(),
492
        );
493
    }
494
495
    /**
496
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
497
     */
498
    private function renderDropdown(array $items): string
499
    {
500
        return self::widget()
501
            ->container(false)
0 ignored issues
show
Bug introduced by
The method container() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Widgets\Menu or Yiisoft\Yii\Widgets\Dropdown. ( Ignorable by Annotation )

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

501
            ->/** @scrutinizer ignore-call */ container(false)
Loading history...
502
            ->dividerAttributes($this->dividerAttributes)
503
            ->headerClass($this->headerClass)
504
            ->headerTag($this->headerTag)
505
            ->itemClass($this->itemClass)
506
            ->itemContainerAttributes($this->itemContainerAttributes)
507
            ->itemContainerTag($this->itemContainerTag)
508
            ->items($items)
509
            ->itemsContainerAttributes(array_merge($this->itemsContainerAttributes))
510
            ->itemTag($this->itemTag)
511
            ->toggleAttributes($this->toggleAttributes)
512
            ->toggleType($this->toggleType)
513
            ->render();
514
    }
515
516
    private function renderHeader(string $label, array $headerAttributes = []): string
517
    {
518
        if ($this->headerClass !== '') {
519
            Html::addCssClass($headerAttributes, $this->headerClass);
520
        }
521
522
        if ($this->headerTag === '') {
523
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
524
        }
525
526
        return $this->renderItemContainer(
527
            Html::normalTag($this->headerTag, $label, $headerAttributes)->encode(false)->render(),
528
        );
529
    }
530
531
    /**
532
     * @param array $item The item to be rendered.
533
     *
534
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
535
     *
536
     * @psalm-param array{
537
     *   label: string,
538
     *   link: string,
539
     *   linkAttributes: array,
540
     *   active: bool,
541
     *   disabled: bool,
542
     *   enclose: bool,
543
     *   headerAttributes: array,
544
     *   itemContainerAttributes: array,
545
     *   toggleAttributes: array,
546
     *   visible: bool,
547
     *   items: array,
548
     * } $item
549
     */
550
    private function renderItem(array $item): string
551
    {
552
        if ($item['visible'] === false) {
553
            return '';
554
        }
555
556
        $lines = [];
557
        $linkAttributes = $item['linkAttributes'];
558
559
        if ($this->itemClass !== '') {
560
            Html::addCssClass($linkAttributes, $this->itemClass);
561
        }
562
563
        if ($item['active']) {
564
            $linkAttributes['aria-current'] = 'true';
565
            Html::addCssClass($linkAttributes, [$this->activeClass]);
566
        }
567
568
        if ($item['disabled']) {
569
            Html::addCssClass($linkAttributes, $this->disabledClass);
570
        }
571
572
        if ($item['items'] === []) {
573
            $lines[] = $this->renderItemContent(
574
                $item['label'],
575
                $item['link'],
576
                $item['enclose'],
577
                $linkAttributes,
578
                $item['headerAttributes'],
579
                $item['itemContainerAttributes'],
580
            );
581
        } else {
582
            $itemContainer = $this->renderItemsContainer($this->renderDropdown($item['items']));
583
            $toggle = $this->renderToggle($item['label'], $item['link'], $item['toggleAttributes']);
584
            $toggleSplitButton = $this->renderToggleSplitButton($item['label']);
585
586
            if ($this->toggleType === 'split' && !str_contains($this->containerClass, 'dropstart')) {
587
                $lines[] = $toggleSplitButton . PHP_EOL . $toggle . PHP_EOL . $itemContainer;
588
            } elseif ($this->toggleType === 'split' && str_contains($this->containerClass, 'dropstart')) {
589
                $lines[] = $toggle . PHP_EOL . $itemContainer . PHP_EOL . $toggleSplitButton;
590
            } else {
591
                $lines[] = $toggle . PHP_EOL . $itemContainer;
592
            }
593
        }
594
595
        /** @psalm-var string[] $lines */
596
        return implode(PHP_EOL, $lines);
597
    }
598
599
    private function renderItemContainer(string $content, array $itemContainerAttributes = []): string
600
    {
601
        if ($this->itemContainerTag === '') {
602
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
603
        }
604
605
        if ($itemContainerAttributes === []) {
606
            $itemContainerAttributes = $this->itemContainerAttributes;
607
        }
608
609
        return Html::normalTag($this->itemContainerTag, $content, $itemContainerAttributes)
610
            ->encode(false)
611
            ->render();
612
    }
613
614
    private function renderItemsContainer(string $content): string
615
    {
616
        $itemsContainerAttributes = $this->itemsContainerAttributes;
617
618
        if ($this->id !== '') {
619
            $itemsContainerAttributes['aria-labelledby'] = $this->id;
620
        }
621
622
        if ($this->itemsContainerTag === '') {
623
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
624
        }
625
626
        return Html::normalTag($this->itemsContainerTag, $content, $itemsContainerAttributes)
627
            ->encode(false)
628
            ->render();
629
    }
630
631
    private function renderItemContent(
632
        string $label,
633
        string $link,
634
        bool $enclose,
635
        array $linkAttributes = [],
636
        array $headerAttributes = [],
637
        array $itemContainerAttributes = [],
638
    ): string {
639
        return match (true) {
640
            $label === '-' => $this->renderDivider(),
641
            $enclose === false => $label,
642
            $link === '' => $this->renderHeader($label, $headerAttributes),
643
            default => $this->renderItemLink($label, $link, $linkAttributes, $itemContainerAttributes),
644
        };
645
    }
646
647
    /**
648
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
649
     *
650
     * @psalm-param array<
651
     *   array-key,
652
     *   array{
653
     *     label: string,
654
     *     link: string,
655
     *     linkAttributes: array,
656
     *     active: bool,
657
     *     disabled: bool,
658
     *     enclose: bool,
659
     *     headerAttributes: array,
660
     *     itemContainerAttributes: array,
661
     *     toggleAttributes: array,
662
     *     visible: bool,
663
     *     items: array,
664
     *   }|string
665
     * > $items
666
     */
667
    private function renderItems(array $items = []): string
668
    {
669
        $lines = [];
670
671
        foreach ($items as $item) {
672
            $line = match (gettype($item)) {
673
                'array' => $this->renderItem($item),
674
                'string' => $this->renderDivider(),
675
            };
676
677
            if ($line !== '') {
678
                $lines[] = $line;
679
            }
680
        }
681
682
        return PHP_EOL . implode(PHP_EOL, $lines);
683
    }
684
685
    private function renderItemLink(
686
        string $label,
687
        string $link,
688
        array $linkAttributes = [],
689
        array $itemContainerAttributes = []
690
    ): string {
691
        $linkAttributes['href'] = $link;
692
693
        if ($this->itemTag === '') {
694
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
695
        }
696
697
        $linkTag = Html::normalTag($this->itemTag, $label, $linkAttributes)->encode(false)->render();
698
699
        return match ($this->itemContainer) {
700
            true => $this->renderItemContainer($linkTag, $itemContainerAttributes),
701
            default => $linkTag,
702
        };
703
    }
704
705
    private function renderToggle(string $label, string $link, array $toggleAttributes = []): string
706
    {
707
        if ($toggleAttributes === []) {
708
            $toggleAttributes = $this->toggleAttributes;
709
        }
710
711
        if ($this->id !== '') {
712
            $toggleAttributes['id'] = $this->id;
713
        }
714
715
        return match ($this->toggleType) {
716
            'link' => $this->renderToggleLink($label, $link, $toggleAttributes),
717
            'split' => $this->renderToggleSplit($label, $toggleAttributes),
718
            default => $this->renderToggleButton($label, $toggleAttributes),
719
        };
720
    }
721
722
    private function renderToggleButton(string $label, array $toggleAttributes = []): string
723
    {
724
        return Button::tag()->attributes($toggleAttributes)->content($label)->type('button')->render();
725
    }
726
727
    private function renderToggleLink(string $label, string $link, array $toggleAttributes = []): string
728
    {
729
        return A::tag()->attributes($toggleAttributes)->content($label)->href($link)->render();
730
    }
731
732
    private function renderToggleSplit(string $label, array $toggleAttributes = []): string
733
    {
734
        return Button::tag()
735
            ->attributes($toggleAttributes)
736
            ->content(Span::tag()->attributes($this->splitButtonSpanAttributes)->content($label))
737
            ->type('button')
738
            ->render();
739
    }
740
741
    private function renderToggleSplitButton(string $label): string
742
    {
743
        return Button::tag()->attributes($this->splitButtonAttributes)->content($label)->type('button')->render();
744
    }
745
}
746