Passed
Pull Request — master (#31)
by Wilmer
02:00
created

Menu   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 1 Features 1
Metric Value
eloc 158
dl 0
loc 469
ccs 161
cts 161
cp 1
rs 4.5599
c 1
b 1
f 1
wmc 58

24 Methods

Rating   Name   Duplication   Size   Complexity  
A withoutHideEmptyItems() 0 5 1
A renderItem() 0 27 4
A withoutEncodeLabels() 0 5 1
A withoutActivateItems() 0 5 1
A buildOptions() 0 7 2
B renderItems() 0 47 11
A buildMenu() 0 15 2
A withActivateParents() 0 5 1
A withEncodeTags() 0 6 1
A run() 0 11 2
A renderIcon() 0 11 2
A isItemActive() 0 3 3
C normalizeItems() 0 37 16
A brand() 0 5 1
A labelTemplate() 0 5 1
A linkTemplate() 0 5 1
A activeCssClass() 0 5 1
A itemOptions() 0 5 1
A subMenuTemplate() 0 5 1
A items() 0 5 1
A options() 0 5 1
A currentPath() 0 5 1
A firstItemCssClass() 0 5 1
A lastItemCssClass() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Menu often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Menu, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bulma;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Html\Html;
9
10
use function array_merge;
11
use function array_values;
12
use function call_user_func;
13
use function count;
14
use function implode;
15
use function strtr;
16
17
/**
18
 * The Bulma menu is a vertical navigation component.
19
 *
20
 * @link https://bulma.io/documentation/components/menu/
21
 */
22
final class Menu extends Widget
23
{
24
    private string $activeCssClass = 'is-active';
25
    private bool $activateItems = true;
26
    private bool $activateParents = false;
27
    private string $brand = '';
28
    private string $currentPath = '';
29
    private string $firstItemCssClass = '';
30
    private array $items = [];
31
    private array $itemOptions = [];
32
    private array $itemsOptions = [];
33
    private string $lastItemCssClass = '';
34
    private string $linkTemplate = '<a href={url}>{icon}{label}</a>';
35
    private string $labelTemplate = '{label}';
36
    private bool $encodeLabels = true;
37
    private bool $encodeTags = false;
38
    private bool $hideEmptyItems = true;
39
    private array $options = [];
40
    private string $subMenuTemplate = "<ul class = menu-list>\n{items}\n</ul>";
41
42
    /**
43
     * Renders the menu.
44
     *
45
     * @return string the result of Widget execution to be outputted.
46
     */
47 20
    protected function run(): string
48
    {
49 20
        $this->items = $this->normalizeItems($this->items, $hasActiveChild);
50
51 20
        if (empty($this->items)) {
52 1
            return '';
53
        }
54
55 19
        $this->buildOptions();
56
57 19
        return $this->buildMenu();
58
    }
59
60
    /**
61
     * Disable activate items according to whether their currentPath.
62
     *
63
     * @return $this
64
     *
65
     * {@see isItemActive}
66
     */
67 1
    public function withoutActivateItems(): self
68
    {
69 1
        $new = clone $this;
70 1
        $new->activateItems = false;
71 1
        return $new;
72
    }
73
74
    /**
75
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
76
     *
77
     * @return $this
78
     */
79 1
    public function withActivateParents(): self
80
    {
81 1
        $new = clone $this;
82 1
        $new->activateParents = true;
83 1
        return $new;
84
    }
85
86
    /**
87
     * The CSS class to be appended to the active menu item.
88
     *
89
     * @var string
90
     *
91
     * @return self
92
     */
93 2
    public function activeCssClass(string $value): self
94
    {
95 2
        $new = clone $this;
96 2
        $new->activeCssClass = $value;
97 2
        return $new;
98
    }
99
100
    /**
101
     * Set render brand custom.
102
     *
103
     * @param string $value
104
     *
105
     * @return self
106
     */
107 1
    public function brand(string $value): self
108
    {
109 1
        $new = clone $this;
110 1
        $new->brand = $value;
111 1
        return $new;
112
    }
113
114
    /**
115
     * Allows you to assign the current path of the url from request controller.
116
     *
117
     * @param string $value
118
     *
119
     * @return self
120
     */
121 7
    public function currentPath(string $value): self
122
    {
123 7
        $new = clone $this;
124 7
        $new->currentPath = $value;
125 7
        return $new;
126
    }
127
128
    /**
129
     * When tags Labels HTML should not be encoded.
130
     *
131
     * @return $this
132
     */
133 1
    public function withoutEncodeLabels(): self
134
    {
135 1
        $new = clone $this;
136 1
        $new->encodeLabels = false;
137 1
        return $new;
138
    }
139
140
    /**
141
     * The CSS class that will be assigned to the first item in the main menu or each submenu. Defaults to null,
142
     * meaning no such CSS class will be assigned.
143
     *
144
     * @var string
145
     *
146
     * @return self
147
     */
148 1
    public function firstItemCssClass(string $value): self
149
    {
150 1
        $new = clone $this;
151 1
        $new->firstItemCssClass = $value;
152 1
        return $new;
153
    }
154
155
    /**
156
     * Whether to hide empty menu items. An empty menu item is one whose `url` option is not set and which has no
157
     * visible child menu items.
158
     *
159
     * @var bool
160
     *
161
     * @return self
162
     */
163 1
    public function withoutHideEmptyItems(): self
164
    {
165 1
        $new = clone $this;
166 1
        $new->hideEmptyItems = false;
167 1
        return $new;
168
    }
169
170
    /**
171
     * List of menu items.
172
     *
173
     * Each menu item should be an array of the following structure:
174
     *
175
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
176
     *   HTML-encoded. If the label is not specified, an empty string will be used.
177
     * - encode: bool, optional, whether this item`s label should be HTML-encoded. This param will override global
178
     *   {@see encodeLabels} param.
179
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
180
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
181
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
182
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
183
     * - active: bool or Closure, optional, whether this menu item is in active state (currently selected). When
184
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
185
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
186
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
187
     *   automatically when the current request is triggered by `url`. For more details, please refer to
188
     *   {@see isItemActive()}.
189
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
190
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
191
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
192
     * - subMenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
193
     *   be replaced with the rendered sub-menu items. If this option is not set, {@see subMenuTemplate} will be used
194
     *   instead.
195
     * - options: array, optional, the HTML attributes for the menu container tag.
196
     * - icon: string, optional, class icon.
197
     * - iconOptions: array, optional, the HTML attributes for the container icon.
198
     *
199
     * @param array $value
200
     *
201
     * @return self
202
     */
203 20
    public function items(array $value): self
204
    {
205 20
        $new = clone $this;
206 20
        $new->items = $value;
207 20
        return $new;
208
    }
209
210
    /**
211
     * List of HTML attributes shared by all menu {@see items}.
212
     *
213
     * If any individual menu item specifies its  `options`, it will be merged with this property before being used to
214
     * generate the HTML attributes for the menu item tag. The following special options are recognized:
215
     *
216
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
217
     *   See also {@see Html::tag()}
218
     *
219
     * @param array $value
220
     *
221
     * @return self
222
     *
223
     * {@see Html::renderTagAttributes() for details on how attributes are being rendered}
224
     */
225 1
    public function itemOptions(array $value): self
226
    {
227 1
        $new = clone $this;
228 1
        $new->itemOptions = $value;
229 1
        return $new;
230
    }
231
232
    /**
233
     * The template used to render the body of a menu which is NOT a link.
234
     *
235
     * In this template, the token `{label}` will be replaced with the label of the menu item.
236
     *
237
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
238
     *
239
     * @param string $value
240
     *
241
     * @return self
242
     */
243 2
    public function labelTemplate(string $value): self
244
    {
245 2
        $new = clone $this;
246 2
        $new->labelTemplate = $value;
247 2
        return $new;
248
    }
249
250
    /**
251
     * The CSS class that will be assigned to the last item in the main menu or each submenu. Defaults to null, meaning
252
     * no such CSS class will be assigned.
253
     *
254
     * @param string $value
255
     *
256
     * @return self
257
     */
258 5
    public function lastItemCssClass(string $value): self
259
    {
260 5
        $new = clone $this;
261 5
        $new->lastItemCssClass = $value;
262 5
        return $new;
263
    }
264
265
    /**
266
     * The template used to render the body of a menu which is a link. In this template, the token `{url}` will be
267
     * replaced with the corresponding link URL; while `{label}` will be replaced with the link text.
268
     *
269
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
270
     *
271
     * @param string $value
272
     *
273
     * @return self
274
     */
275 2
    public function linkTemplate(string $value): self
276
    {
277 2
        $new = clone $this;
278 2
        $new->linkTemplate = $value;
279 2
        return $new;
280
    }
281
282
    /**
283
     * The HTML attributes for the menu's container tag. The following special options are recognized:
284
     *
285
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
286
     *   See also {@see Html::tag()}.
287
     *
288
     * @param array $value
289
     *
290
     * @return self
291
     *
292
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
293
     */
294 1
    public function options(array $value): self
295
    {
296 1
        $new = clone $this;
297 1
        $new->options = $value;
298 1
        return $new;
299
    }
300
301
    /**
302
     * The template used to render a list of sub-menus.
303
     *
304
     * In this template, the token `{items}` will be replaced with the rendered sub-menu items.
305
     *
306
     * @param string $value
307
     *
308
     * @return self
309
     */
310 1
    public function subMenuTemplate(string $value): self
311
    {
312 1
        $new = clone $this;
313 1
        $new->subMenuTemplate = $value;
314 1
        return $new;
315
    }
316
317
    /**
318
     * Allows you to enable the encoding tags html.
319
     *
320
     * @return self
321
     */
322 1
    public function withEncodeTags(): self
323
    {
324 1
        $new = clone $this;
325 1
        $new->encodeTags = true;
326
327 1
        return $new;
328
    }
329
330 19
    private function renderItems(array $items): string
331
    {
332 19
        $n = count($items);
333 19
        $lines = [];
334
335 19
        foreach ($items as $i => $item) {
336 19
            $class = [];
337 19
            $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
338 19
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
339 19
            $tag = ArrayHelper::remove($options, 'tag', 'li');
340
341
342 19
            if ($item['active']) {
343 10
                $linkOptions = $this->addOptions($linkOptions, $this->activeCssClass);
344
            }
345
346 19
            if ($i === 0 && $this->firstItemCssClass !== '') {
347 1
                $class[] = $this->firstItemCssClass;
348
            }
349
350 19
            if ($i === $n - 1 && $this->lastItemCssClass !== '') {
351 5
                $class[] = $this->lastItemCssClass;
352
            }
353
354 19
            Html::addCssClass($options, $class);
355
356 19
            $menu = $this->renderItem($item, $linkOptions);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type object; however, parameter $item of Yiisoft\Yii\Bulma\Menu::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

356
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item, $linkOptions);
Loading history...
357
358 19
            if (!empty($item['items'])) {
359 10
                $subMenuTemplate = ArrayHelper::getValue($item, 'subMenuTemplate', $this->subMenuTemplate);
360 10
                $menu .= strtr($subMenuTemplate, [
361 10
                    '{items}' => $this->renderItems($item['items']),
362
                ]);
363
            }
364
365 19
            if (isset($item['label']) && !isset($item['url'])) {
366 12
                if (!empty($menu)) {
367 12
                    $lines[] = $menu;
368
                } else {
369 12
                    $lines[] = $item['label'];
370
                }
371
            } else {
372 19
                $lines[] = Html::tag($tag, $menu, $options);
373
            }
374
        }
375
376 19
        return implode("\n", $lines);
377
    }
378
379 19
    private function renderItem(array $item, array $linkOptions): string
380
    {
381 19
        if (isset($item['url'])) {
382 19
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
383
384 19
            $htmlIcon = '';
385
386 19
            if (isset($item['icon'])) {
387 2
                $htmlIcon = $this->renderIcon($item['icon'], $item['iconOptions']);
388
            }
389
390 19
            if (Html::renderTagAttributes($linkOptions) !== '') {
391 11
                $url = '"' . Html::encode($item['url']) . '"' . Html::renderTagAttributes($linkOptions);
392
            } else {
393 18
                $url = '"' . Html::encode($item['url']) . '"';
394
            }
395 19
            return strtr($template, [
396 19
                '{url}' => $url,
397 19
                '{label}' => $item['label'],
398 19
                '{icon}' => $htmlIcon,
399
            ]);
400
        }
401
402 12
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
403
404 12
        return strtr($template, [
405 12
            '{label}' => Html::tag('p', $item['label'], ['class' => 'menu-label']) . "\n",
406
        ]);
407
    }
408
409 20
    private function normalizeItems(array $items, ?bool &$active): array
410
    {
411 20
        foreach ($items as $i => $item) {
412 19
            if (isset($item['visible']) && !$item['visible']) {
413 1
                unset($items[$i]);
414
            } else {
415 19
                $item['label'] = $item['label'] ?? '';
416 19
                $encodeLabel = $item['encode'] ?? $this->encodeLabels;
417 19
                $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
418 19
                $hasActiveChild = false;
419
420 19
                if (isset($item['items'])) {
421 10
                    $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
422 10
                    if (empty($items[$i]['items']) && $this->hideEmptyItems) {
423 1
                        unset($items[$i]['items']);
424 1
                        if (!isset($item['url'])) {
425 1
                            unset($items[$i]);
426 1
                            continue;
427
                        }
428
                    }
429
                }
430
431 19
                if (!isset($item['active'])) {
432 18
                    if (($this->activateParents && $hasActiveChild) || ($this->activateItems && $this->isItemActive($item))) {
433 7
                        $active = $items[$i]['active'] = true;
434
                    } else {
435 18
                        $items[$i]['active'] = false;
436
                    }
437 3
                } elseif (is_callable($item['active'])) {
438 1
                    $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
439 3
                } elseif ($item['active']) {
440 2
                    $active = $item['active'];
441
                }
442
            }
443
        }
444
445 20
        return array_values($items);
446
    }
447
448 19
    private function isItemActive(array $item): bool
449
    {
450 19
        return isset($item['url']) && $item['url'] === $this->currentPath && $this->activateItems;
451
    }
452
453 2
    private function renderIcon(string $icon, array $iconOptions): string
454
    {
455 2
        $html = '';
456
457 2
        if ($icon !== '') {
458 2
            $html = Html::beginTag('span', $iconOptions) .
459 2
                Html::tag('i', '', ['class' => $icon]) .
460 2
                Html::endTag('span');
461
        }
462
463 2
        return $html;
464
    }
465
466 19
    private function buildOptions(): void
467
    {
468 19
        $this->options = $this->addOptions($this->options, 'menu');
469 19
        $this->itemsOptions = $this->addOptions($this->itemsOptions, 'menu-list');
470
471 19
        if ($this->encodeTags === false) {
472 18
            $this->itemOptions['encode'] = false;
473
        }
474 19
    }
475
476 19
    private function buildMenu(): string
477
    {
478 19
        $tag = ArrayHelper::remove($this->options, 'tag', 'ul');
479 19
        $html = Html::beginTag('aside', $this->options) . "\n";
480
481 19
        if ($this->brand !== '') {
482 1
            $html .= $this->brand . "\n";
483
        }
484
485 19
        $html .= Html::beginTag($tag, $this->itemsOptions) . "\n";
486 19
        $html .=  $this->renderItems($this->items) . "\n";
487 19
        $html .= Html::endTag($tag) . "\n";
488 19
        $html .= Html::endTag('aside');
489
490 19
        return $html;
491
    }
492
}
493