Passed
Push — master ( c03bea...c0a5b3 )
by Rustam
01:44
created

Menu::normalizeItems()   C

Complexity

Conditions 17
Paths 66

Size

Total Lines 41
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 33.4409

Importance

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

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