Passed
Push — master ( b3e21b...2e544e )
by Alexander
01:42
created

Menu::normalizeItems()   C

Complexity

Conditions 17
Paths 66

Size

Total Lines 50
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 25.8195

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 33
c 1
b 0
f 0
nc 66
nop 2
dl 0
loc 50
ccs 22
cts 32
cp 0.6875
crap 25.8195
rs 5.2166

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

398
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item);
Loading history...
399
400 7
            if (!empty($item['items'])) {
401
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
402
                $menu .= strtr($submenuTemplate, [
403
                    '{items}' => $this->renderItems($item['items']),
404
                ]);
405
            }
406
407 7
            $lines[] = Html::tag($tag, $menu, $options);
408
        }
409
410 7
        return implode("\n", $lines);
411
    }
412
413
    /**
414
     * Renders the content of a menu item.
415
     *
416
     * Note that the container and the sub-menus are not rendered here.
417
     *
418
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
419
     * item.
420
     *
421
     * @return string the rendering result
422
     */
423 7
    protected function renderItem(array $item): string
424
    {
425 7
        if (isset($item['url'])) {
426 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
427
428 7
            return strtr($template, [
429 7
                '{url}'   => Html::encode($item['url']),
430 7
                '{label}' => $item['label'],
431
            ]);
432
        }
433
434 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
435
436 2
        return strtr($template, [
437 2
            '{label}' => $item['label'],
438
        ]);
439
    }
440
441
    /**
442
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
443
     *
444
     * @param array $items  the items to be normalized.
445
     * @param bool|null $active whether there is an active child menu item.
446
     *
447
     * @return array the normalized menu items
448
     */
449 7
    protected function normalizeItems(array $items, ?bool &$active): array
450
    {
451 7
        foreach ($items as $i => $item) {
452 7
            if (isset($item['visible']) && !$item['visible']) {
453
                unset($items[$i]);
454
                continue;
455
            }
456
457 7
            if (!isset($item['label'])) {
458
                $item['label'] = '';
459
            }
460
461 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
462 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
463 7
            $hasActiveChild = false;
464
465 7
            if (isset($item['items'])) {
466
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
467
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
468
                    unset($items[$i]['items']);
469
                    if (!isset($item['url'])) {
470
                        unset($items[$i]);
471
                        continue;
472
                    }
473
                }
474
            }
475
476 7
            if (!isset($item['active'])) {
477
                if (
478 6
                    ($this->activateParents && $hasActiveChild)
479 6
                    || ($this->activateItems && $this->isItemActive($item))
480
                ) {
481
                    $active = $items[$i]['active'] = true;
482
                } else {
483 6
                    $items[$i]['active'] = false;
484
                }
485 3
            } elseif ($item['active'] instanceof Closure) {
486 1
                $active = $items[$i]['active'] = call_user_func(
487 1
                    $item['active'],
488 1
                    $item,
489 1
                    $hasActiveChild,
490 1
                    $this->isItemActive($item),
491 1
                    $this
492
                );
493 3
            } elseif ($item['active']) {
494 3
                $active = true;
495
            }
496
        }
497
498 7
        return array_values($items);
499
    }
500
501
    /**
502
     * Checks whether a menu item is active.
503
     *
504
     * This is done by checking match that specified in the `url` option of the menu item.
505
     *
506
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
507
     *
508
     * @param array $item the menu item to be checked
509
     * @param bool $active returns the result when the item is active
510
     *
511
     * @return bool whether the menu item is active
512
     */
513 7
    protected function isItemActive(array $item, bool $active = false): bool
514
    {
515
        if (
516 7
            $this->activateItems
517 7
            && $this->currentPath !== '/'
518 7
            && isset($item['url'])
519 7
            && $item['url'] === $this->currentPath
520
        ) {
521
            $active = true;
522
        }
523
524 7
        return $active;
525
    }
526
}
527