Passed
Pull Request — master (#52)
by Wilmer
04:13 queued 02:05
created

Dropdown::renderItems()   C

Complexity

Conditions 13
Paths 33

Size

Total Lines 101
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 55
CRAP Score 13.0009

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 58
c 1
b 0
f 0
nc 33
nop 0
dl 0
loc 101
ccs 55
cts 56
cp 0.9821
crap 13.0009
rs 6.6166

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\Bulma;
6
7
use InvalidArgumentException;
8
use ReflectionException;
9
use Yiisoft\Html\Html;
10
use Yiisoft\Html\Tag\A;
11
use Yiisoft\Html\Tag\Button;
12
use Yiisoft\Html\Tag\CustomTag;
13
use Yiisoft\Html\Tag\Div;
14
use Yiisoft\Html\Tag\Span;
15
use Yiisoft\Widget\Widget;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Yii\Bulma\Widget. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
17
use function array_merge;
18
use function implode;
19
20
/**
21
 * The dropdown component is a container for a dropdown button and a dropdown menu.
22
 *
23
 * @link https://bulma.io/documentation/components/dropdown/
24
 */
25
final class Dropdown extends Widget
26
{
27
    private array $attributes = [];
28
    private string $autoIdPrefix = 'w';
29
    private array $buttonAttributes = [];
30
    private array $buttonIconAttributes = ['class' => 'icon is-small'];
31
    private string $buttonIconCssClass = '';
32
    private string $buttonIconText = '&#8595;';
33
    private string $buttonLabel = 'Clic Me';
34
    private array $buttonLabelAttributes = [];
35
    private string $dividerCssClass = 'dropdown-divider';
36
    private string $dropdownCssClass = 'dropdown';
37
    private string $dropdownContentCssClass = 'dropdown-content';
38
    private string $dropdownItemActiveCssClass = 'is-active';
39
    private string $dropdownItemCssClass = 'dropdown-item';
40
    private string $dropdownItemDisabledStyleCss = 'opacity:.65;pointer-events:none;';
41
    private string $dropdownItemHeaderCssClass = 'dropdown-header';
42
    private string $dropdownMenuCssClass = 'dropdown-menu';
43
    private string $dropdownTriggerCssClass = 'dropdown-trigger';
44
    private bool $encloseByContainer = true;
45
    private array $items = [];
46
    private bool $submenu = false;
47
    private array $submenuAttributes = [];
48
49
    /**
50
     * Returns a new instance with the specified attributes.
51
     *
52
     * @param array $value The HTML attributes for the widget container nav tag.
53
     *
54
     * @return self
55
     *
56
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
57
     */
58 3
    public function attributes(array $value): self
59
    {
60 3
        $new = clone $this;
61 3
        $new->attributes = $value;
62 3
        return $new;
63
    }
64
65
    /**
66
     * Returns a new instance with the specified prefix to the automatically generated widget IDs.
67
     *
68
     * @param string $value The prefix to the automatically generated widget IDs.
69
     *
70
     * @return self
71
     */
72 1
    public function autoIdPrefix(string $value): self
73
    {
74 1
        $new = clone $this;
75 1
        $new->autoIdPrefix = $value;
76 1
        return $new;
77
    }
78
79
    /**
80
     * The HTML attributes for the dropdown button. The following special options are recognized.
81
     *
82
     * @param array $value
83
     *
84
     * @return self
85
     */
86 2
    public function buttonAttributes(array $value): self
87
    {
88 2
        $new = clone $this;
89 2
        $new->buttonAttributes = $value;
90 2
        return $new;
91
    }
92
93
    /**
94
     * The HTML attributes for the dropdown icon button. The following special options are recognized.
95
     *
96
     * @param array $value
97
     *
98
     * @return self
99
     */
100 2
    public function buttonIconAttributes(array $value): self
101
    {
102 2
        $new = clone $this;
103 2
        $new->buttonIconAttributes = $value;
104 2
        return $new;
105
    }
106
107
    /**
108
     * Set icon css class for the dropdown button.
109
     *
110
     * @param string $value
111
     *
112
     * @return self
113
     */
114 2
    public function buttonIconCssClass(string $value): self
115
    {
116 2
        $new = clone $this;
117 2
        $new->buttonIconCssClass = $value;
118 2
        return $new;
119
    }
120
121
    /**
122
     * Set icon text for the dropdown button.
123
     *
124
     * @param string $value
125
     *
126
     * @return self
127
     */
128 3
    public function buttonIconText(string $value): self
129
    {
130 3
        $new = clone $this;
131 3
        $new->buttonIconText = $value;
132 3
        return $new;
133
    }
134
135
    /**
136
     * Set label for the dropdown button.
137
     *
138
     * @param string $value
139
     *
140
     * @return self
141
     */
142 2
    public function buttonLabel(string $value): self
143
    {
144 2
        $new = clone $this;
145 2
        $new->buttonLabel = $value;
146 2
        return $new;
147
    }
148
149
    /**
150
     * The HTML attributes for the dropdown button label. The following special options are recognized.
151
     *
152
     * @param array $value
153
     *
154
     * @return self
155
     */
156 2
    public function buttonLabelAttributes(array $value): self
157
    {
158 2
        $new = clone $this;
159 2
        $new->buttonLabelAttributes = $value;
160 2
        return $new;
161
    }
162
163
    /**
164
     * A horizontal line to separate dropdown items.
165
     *
166
     * @return self
167
     */
168 11
    public function dividerCssClass(string $value): self
169
    {
170 11
        $new = clone $this;
171 11
        $new->dividerCssClass = $value;
172 11
        return $new;
173
    }
174
175 9
    public function dropdownCssClass(string $value): self
176
    {
177 9
        $new = clone $this;
178 9
        $new->dropdownCssClass = $value;
179 9
        return $new;
180
    }
181
182
    /**
183
     * The dropdown box, with a white background and a shadow.
184
     *
185
     * @return self
186
     *
187
     * @link https://bulma.io/documentation/components/dropdown/#dropdown-content
188
     */
189 2
    public function dropdownContentCssClass(string $value): self
190
    {
191 2
        $new = clone $this;
192 2
        $new->dropdownContentCssClass = $value;
193 2
        return $new;
194
    }
195
196 2
    public function dropdownItemActiveCssClass(string $value): self
197
    {
198 2
        $new = clone $this;
199 2
        $new->dropdownItemActiveCssClass = $value;
200 2
        return $new;
201
    }
202
203 11
    public function dropdownItemCssClass(string $value): self
204
    {
205 11
        $new = clone $this;
206 11
        $new->dropdownItemCssClass = $value;
207 11
        return $new;
208
    }
209
210 2
    public function dropdownItemDisabledStyleCss(string $value): self
211
    {
212 2
        $new = clone $this;
213 2
        $new->dropdownItemDisabledStyleCss = $value;
214 2
        return $new;
215
    }
216
217 2
    public function dropdownItemHeaderCssClass(string $value): self
218
    {
219 2
        $new = clone $this;
220 2
        $new->dropdownItemHeaderCssClass = $value;
221 2
        return $new;
222
    }
223
224
    /**
225
     * The toggable menu, hidden by default.
226
     *
227
     * @return self
228
     */
229 2
    public function dropdownMenuCssClass(string $value): self
230
    {
231 2
        $new = clone $this;
232 2
        $new->dropdownMenuCssClass = $value;
233 2
        return $new;
234
    }
235
236
    /**
237
     * The toggable menu, hidden by default.
238
     *
239
     * @return self
240
     */
241 2
    public function dropdownTriggerCssClass(string $value): self
242
    {
243 2
        $new = clone $this;
244 2
        $new->dropdownTriggerCssClass = $value;
245 2
        return $new;
246
    }
247
248
    /**
249
     * Returns a new instance with the specified ID of the widget.
250
     *
251
     * @param string $value The ID of the widget.
252
     *
253
     * @return self
254
     */
255 2
    public function id(string $value): self
256
    {
257 2
        $new = clone $this;
258 2
        $new->attributes['id'] = $value;
259 2
        return $new;
260
    }
261
262
    /**
263
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
264
     * single menu with the following structure:
265
     *
266
     * - label: string, required, the label of the item link.
267
     * - encode: bool, optional, whether to HTML-encode item label.
268
     * - url: string|array, optional, the URL of the item link. This will be processed by {@see currentPath}.
269
     *   If not set, the item will be treated as a menu header when the item has no sub-menu.
270
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
271
     * - urlAttributes: array, optional, the HTML attributes of the item link.
272
     * - attributes: array, optional, the HTML attributes of the item.
273
     * - items: array, optional, the submenu items. The structure is the same as this property.
274
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
275
     * - submenuOptions: array, optional, the HTML attributes for sub-menu container tag. If specified it will be
276
     *   merged with {@see submenuOptions}.
277
     *
278
     * To insert divider use `-`.
279
     *
280
     * @param array $value
281
     *
282
     * @return self
283
     */
284 29
    public function items(array $value): self
285
    {
286 29
        $new = clone $this;
287 29
        $new->items = $value;
288 29
        return $new;
289
    }
290
291
    /**
292
     * Set if it is a submenu or subdropdown,
293
     *
294
     * @param bool $value
295
     *
296
     * @return self
297
     */
298 3
    public function submenu(bool $value): self
299
    {
300 3
        $new = clone $this;
301 3
        $new->submenu = $value;
302 3
        return $new;
303
    }
304
305
    /**
306
     * The HTML attributes for sub-menu container tags.
307
     *
308
     * @param array $value
309
     *
310
     * @return self
311
     */
312 3
    public function submenuAttributes(array $value): self
313
    {
314 3
        $new = clone $this;
315 3
        $new->submenuAttributes = $value;
316 3
        return $new;
317
    }
318
319
    /**
320
     * If the widget should be unclosed by container.
321
     *
322
     * @param bool $value
323
     *
324
     * @return self
325
     */
326 10
    public function unClosedByContainer(bool $value = false): self
327
    {
328 10
        $new = clone $this;
329 10
        $new->encloseByContainer = $value;
330 10
        return $new;
331
    }
332
333
    /**
334
     * @throws ReflectionException
335
     */
336 28
    private function renderDropdown(): string
337
    {
338 28
        $attributes = $this->attributes;
339
340
        /** @var string */
341 28
        $id = $attributes['id'] ?? Html::generateId($this->autoIdPrefix) . '-dropdown';
342 28
        unset($attributes['id']);
343
344 28
        if ($this->encloseByContainer) {
345 20
            Html::addCssClass($attributes, $this->dropdownCssClass);
346 20
            $html = Div::tag()
347 20
                ->attributes($attributes)
348 20
                ->content(PHP_EOL . $this->renderDropdownTrigger($id) . PHP_EOL)
349 19
                ->encode(false)
350 19
                ->render();
351
        } else {
352 9
            $html = $this->renderItems();
353
        }
354
355 27
        return $html;
356
    }
357
358
    /**
359
     * Render dropdown button.
360
     *
361
     * @return string the rendering toggle button.
362
     *
363
     * @link https://bulma.io/documentation/components/dropdown/#hoverable-or-toggable
364
     */
365 19
    private function renderDropdownButton(string $id): string
366
    {
367 19
        $buttonAttributes = $this->buttonAttributes;
368
369 19
        Html::addCssClass($buttonAttributes, 'button');
370
371 19
        $buttonAttributes['aria-haspopup'] = 'true';
372 19
        $buttonAttributes['aria-controls'] = $id;
373
374 19
        return Button::tag()
375 19
            ->attributes($buttonAttributes)
376 19
            ->content(
377 19
                $this->renderLabelButton(
378 19
                    $this->buttonLabel,
379 19
                    $this->buttonLabelAttributes,
380 19
                    $this->buttonIconText,
381 19
                    $this->buttonIconCssClass,
382 19
                    $this->buttonIconAttributes,
383
                )
384
            )
385 19
            ->encode(false)
386 19
            ->render() . PHP_EOL;
387
    }
388
389 2
    public function renderDropdownButtonLink(): string
390
    {
391 2
        return A::tag()
392 2
            ->class($this->dropdownItemCssClass)
393 2
            ->content(
394 2
                $this->renderLabelButton(
395 2
                    $this->buttonLabel,
396 2
                    $this->buttonLabelAttributes,
397 2
                    $this->buttonIconText,
398 2
                    $this->buttonIconCssClass,
399 2
                    $this->buttonIconAttributes,
400
                )
401
            )
402 2
            ->encode(false)
403 2
            ->render() . PHP_EOL;
404
    }
405
406
    /**
407
     * @throws ReflectionException
408
     */
409 28
    protected function run(): string
410
    {
411 28
        return $this->renderDropdown();
412
    }
413
414
    /**
415
     * @throws ReflectionException
416
     */
417 20
    private function renderDropdownContent(): string
418
    {
419 20
        return Div::tag()
420 20
            ->class($this->dropdownContentCssClass)
421 20
            ->content(PHP_EOL . $this->renderItems() . PHP_EOL)
422 19
            ->encode(false)
423 19
            ->render();
424
    }
425
426
    /**
427
     * @throws ReflectionException
428
     */
429 20
    private function renderDropdownMenu(string $id): string
430
    {
431 20
        return Div::tag()
432 20
            ->class($this->dropdownMenuCssClass)
433 20
            ->content(PHP_EOL . $this->renderDropdownContent() . PHP_EOL)
434 19
            ->encode(false)
435 19
            ->id($id)
436 19
            ->render();
437
    }
438
439
    /**
440
     * @throws ReflectionException
441
     */
442 20
    private function renderDropdownTrigger(string $id): string
443
    {
444 20
        if ($this->submenu !== true) {
445 19
            $button = $this->renderDropdownButton($id);
446
        } else {
447 2
            $button = $this->renderDropdownButtonLink();
448
        }
449
450 20
        return Div::tag()
451 20
            ->class($this->dropdownTriggerCssClass)
452 20
            ->content(PHP_EOL . $button)
453 20
            ->encode(false)
454 20
            ->render() . PHP_EOL . $this->renderDropdownMenu($id);
455
    }
456
457
    /**
458
     * Renders menu items.
459
     *
460
     * @throws InvalidArgumentException|ReflectionException if the label option is not specified in one of the items.
461
     *
462
     * @return string the rendering result.
463
     */
464 28
    private function renderItems(): string
465
    {
466 28
        $lines = [];
467
468
        /** @var array|string $item */
469 28
        foreach ($this->items as $item) {
470 28
            if ($item === '-') {
471 4
                $lines[] = CustomTag::name('hr')->class($this->dividerCssClass)->render();
472
            } else {
473 28
                if (!isset($item['label']) && $item !== '-') {
474 1
                    throw new InvalidArgumentException('The "label" option is required.');
475
                }
476
477
                /** @var string */
478 27
                $itemLabel = $item['label'] ?? '';
479
480 27
                if (isset($item['encode']) && $item['encode'] === true) {
481 1
                    $itemLabel = Html::encode($itemLabel);
482
                }
483
484
                /** @var array */
485 27
                $items = $item['items'] ?? [];
486
487
                /** @var array */
488 27
                $urlAttributes = $item['urlAttributes'] ?? [];
489
490
                /** @var string */
491 27
                $iconText = $item['iconText'] ?? '';
492
493
                /** @var string */
494 27
                $iconCssClass = $item['iconCssClass'] ?? '';
495
496
                /** @var array */
497 27
                $iconAttributes = $item['iconAttributes'] ?? [];
498
499
                /** @var string */
500 27
                $url = $item['url'] ?? '';
501
502
                /** @var bool */
503 27
                $active = $item['active'] ?? false;
504
505
                /** @var bool */
506 27
                $disabled = $item['disable'] ?? false;
507
508
                /** @var bool */
509 27
                $enclose = $item['enclose'] ?? true;
510
511
                /** @var bool */
512 27
                $submenu = $item['submenu'] ?? false;
513
514 27
                $itemLabel = $this->renderLabelItem($itemLabel, $iconText, $iconCssClass, $iconAttributes);
515
516 27
                Html::addCssClass($urlAttributes, $this->dropdownItemCssClass);
517
518 27
                if ($disabled) {
519 2
                    Html::addCssStyle($urlAttributes, $this->dropdownItemDisabledStyleCss);
520 26
                } elseif ($active) {
521 5
                    Html::addCssClass($urlAttributes, $this->dropdownItemActiveCssClass);
522
                }
523
524 27
                if ($items === []) {
525 27
                    if ($itemLabel === '-') {
526 1
                        $content = CustomTag::name('hr')->class($this->dividerCssClass)->render();
527 27
                    } elseif ($enclose === false) {
528 1
                        $content = $itemLabel;
529 27
                    } elseif ($url === '') {
530 1
                        $content = CustomTag::name('h6')
531 1
                            ->class($this->dropdownItemHeaderCssClass)
532 1
                            ->content($itemLabel)
533 1
                            ->encode(null)
534 1
                            ->render();
535
                    } else {
536 26
                        $content = A::tag()
537 26
                            ->attributes($urlAttributes)
538 26
                            ->content($itemLabel)
539 26
                            ->encode(false)
540 26
                            ->url($url)
541 26
                            ->render();
542
                    }
543
544 27
                    $lines[] = $content;
545
                } else {
546
                    /** @var array */
547 2
                    $submenuAttributes = $item['submenuAttributes'] ?? [];
548 2
                    $submenuAttributes = array_merge($this->submenuAttributes, $submenuAttributes);
549
550 2
                    $dropdown = self::widget();
551
552
                    $lines[] = $dropdown
553 2
                        ->attributes($this->attributes)
0 ignored issues
show
Bug introduced by
The method attributes() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Yii\Bulma\Dropdown or Yiisoft\Yii\Bulma\Breadcrumbs or Yiisoft\Yii\Bulma\Nav or Yiisoft\Yii\Bulma\NavBar. ( Ignorable by Annotation )

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

553
                        ->/** @scrutinizer ignore-call */ attributes($this->attributes)
Loading history...
554 2
                        ->dividerCssClass($this->dividerCssClass)
555 2
                        ->dropdownItemCssClass($this->dropdownItemCssClass)
556 2
                        ->items($items)
557 2
                        ->submenu($submenu)
558 2
                        ->submenuAttributes($submenuAttributes)
559 2
                        ->render();
560
                }
561
            }
562
        }
563
564 27
        return implode(PHP_EOL, $lines);
565
    }
566
567 20
    private function renderLabelButton(
568
        string $label,
569
        array $labelAttributes,
570
        string $iconText,
571
        string $iconCssClass,
572
        array $iconAttributes = []
573
    ): string {
574 20
        $html = '';
575
576 20
        if ($label !== '') {
577 20
            $html = PHP_EOL . Span::tag()
578 20
                ->attributes($labelAttributes)
579 20
                ->content($label)
580 20
                ->encode(false)
581 20
                ->render();
582
        }
583
584 20
        if ($iconText !== '' || $iconCssClass !== '') {
585 20
            $html .= PHP_EOL .
586 20
                Span::tag()
587 20
                    ->attributes($iconAttributes)
588 20
                    ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
589 20
                    ->encode(false)
590 20
                    ->render();
591
        }
592
593 20
        return $html . PHP_EOL;
594
    }
595
596 27
    private function renderLabelItem(
597
        string $label,
598
        string $iconText,
599
        string $iconCssClass,
600
        array $iconAttributes = []
601
    ): string {
602 27
        $html = '';
603
604 27
        if ($iconText !== '' || $iconCssClass !== '') {
605 1
            $html = Span::tag()
606 1
                ->attributes($iconAttributes)
607 1
                ->content(CustomTag::name('i')->class($iconCssClass)->content($iconText)->encode(false)->render())
608 1
                ->encode(false)
609 1
                ->render();
610
        }
611
612 27
        if ($label !== '') {
613 27
            $html .= $label;
614
        }
615
616 27
        return $html;
617
    }
618
}
619