Passed
Push — master ( fd7f0a...16b7fb )
by Alexander
02:29
created

Menu::normalizeItems()   C

Complexity

Conditions 17
Paths 66

Size

Total Lines 50
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 32.2402

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 20
cts 32
cp 0.625
crap 32.2402
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 function array_merge;
8
use function array_values;
9
use function call_user_func;
10
use Closure;
11
use function count;
12
13
use function implode;
14
use JsonException;
15
use function strtr;
16
use Yiisoft\Arrays\ArrayHelper;
17
use Yiisoft\Html\Html;
18
use Yiisoft\Widget\Widget;
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 string the template used to render a list of sub-menus.
44
     *
45
     * In this template, the token `{items}` will be replaced with the rendered sub-menu items.
46
     */
47
    private string $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
48
    private array $items = [];
49
    private array $itemOptions = [];
50
    private string $linkTemplate = '<a href="{url}">{label}</a>';
51
    private string $labelTemplate = '{label}';
52
    private bool $encodeLabels = true;
53
    private string $activeCssClass = 'active';
54
    private bool $activateItems = true;
55
    private bool $activateParents = false;
56
    private ?string $currentPath = null;
57
    private bool $hideEmptyItems = true;
58
    private array $options = [];
59
    private ?string $firstItemCssClass = null;
60
    private ?string $lastItemCssClass = null;
61
62
    /**
63
     * Renders the menu.
64
     *
65
     * @throws JsonException
66
     *
67
     * @return string the result of Widget execution to be outputted.
68
     */
69 7
    protected function run(): string
70
    {
71 7
        $items = $this->normalizeItems($this->items, $hasActiveChild);
72
73 7
        if (empty($items)) {
74
            return '';
75
        }
76
77 7
        $options = $this->options;
78 7
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
79
80 7
        return empty($tag)
81 1
            ? $this->renderItems($items)
82 7
            : Html::tag($tag, $this->renderItems($items), $options)->encode(false)->render()
83
        ;
84
    }
85
86
    /**
87
     * @param bool $value whether to automatically activate items according to whether their route setting matches the
88
     * currently requested route.
89
     *
90
     * @return $this
91
     */
92
    public function activateItems(bool $value): self
93
    {
94
        $this->activateItems = $value;
95
96
        return $this;
97
    }
98
99
    /**
100
     * @param bool $value whether to activate parent menu items when one of the corresponding child menu items is
101
     * active. The activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
102
     *
103
     * @return $this
104
     */
105
    public function activateParents(bool $value): self
106
    {
107
        $this->activateParents = $value;
108
109
        return $this;
110
    }
111
112
    /**
113
     * @param string $value the CSS class to be appended to the active menu item.
114
     *
115
     * @return $this
116
     */
117 2
    public function activeCssClass(string $value): self
118
    {
119 2
        $this->activeCssClass = $value;
120
121 2
        return $this;
122
    }
123
124
    /**
125
     * @param string|null $value allows you to assign the current path of the url from request controller.
126
     *
127
     * @return $this
128
     */
129
    public function currentPath(?string $value): self
130
    {
131
        $this->currentPath = $value;
132
133
        return $this;
134
    }
135
136
    /**
137
     * @param bool $value whether the labels for menu items should be HTML-encoded.
138
     *
139
     * @return $this
140
     */
141 5
    public function encodeLabels(bool $value): self
142
    {
143 5
        $this->encodeLabels = $value;
144
145 5
        return $this;
146
    }
147
148
    /**
149
     * @param string|null $value the CSS class that will be assigned to the first item in the main menu or each submenu.
150
     * Defaults to null, meaning no such CSS class will be assigned.
151
     *
152
     * @return $this
153
     */
154
    public function firstItemCssClass(?string $value): self
155
    {
156
        $this->firstItemCssClass = $value;
157
158
        return $this;
159
    }
160
161
    /**
162
     * @param bool $value whether to hide empty menu items. An empty menu item is one whose `url` option is not set and
163
     * which has no visible child menu items.
164
     *
165
     * @return $this
166
     */
167
    public function hideEmptyItems(bool $value): self
168
    {
169
        $this->hideEmptyItems = $value;
170
171
        return $this;
172
    }
173
174
    /**
175
     * @param array $value list of menu items. Each menu item should be an array of the following structure:
176
     *
177
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
178
     *   HTML-encoded. If the label is not specified, an empty string will be used.
179
     * - encode: bool, optional, whether this item`s label should be HTML-encoded. This param will override global
180
     *   {@see encodeLabels} param.
181
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
182
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
183
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
184
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
185
     * - active: bool or Closure, optional, whether this menu item is in active state (currently selected). When
186
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
187
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
188
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
189
     *   automatically when the current request is triggered by `url`. For more details, please refer to
190
     *   {@see isItemActive()}.
191
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
192
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
193
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
194
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
195
     *   be replaced with the rendered sub-menu items. If this option is not set, {@see submenuTemplate} will be used
196
     *   instead.
197
     * - options: array, optional, the HTML attributes for the menu container tag.
198
     *
199
     * @return $this
200
     */
201 7
    public function items(array $value): self
202
    {
203 7
        $this->items = $value;
204
205 7
        return $this;
206
    }
207
208
    /**
209
     * @param array $value list of HTML attributes shared by all menu {@see items}. If any individual menu item
210
     * specifies its `options`, it will be merged with this property before being used to generate the HTML attributes
211
     * for the menu item tag. The following special options are recognized:
212
     *
213
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
214
     *   See also {@see \Yiisoft\Html\Html::tag()}
215
     *
216
     * @return $this
217
     *
218
     * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
219
     */
220 1
    public function itemOptions(array $value): self
221
    {
222 1
        $this->itemOptions = $value;
223
224 1
        return $this;
225
    }
226
227
    /**
228
     * @param string $value the template used to render the body of a menu which is NOT a link.
229
     *
230
     * In this template, the token `{label}` will be replaced with the label of the menu item.
231
     *
232
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
233
     *
234
     * @return $this
235
     */
236 2
    public function labelTemplate(string $value): self
237
    {
238 2
        $this->labelTemplate = $value;
239
240 2
        return $this;
241
    }
242
243
    /**
244
     * @param string|null $value the CSS class that will be assigned to the last item in the main menu or each submenu.
245
     * Defaults to null, meaning no such CSS class will be assigned.
246
     *
247
     * @return $this
248
     */
249
    public function lastItemCssClass(?string $value): self
250
    {
251
        $this->lastItemCssClass = $value;
252
253
        return $this;
254
    }
255
256
    /**
257
     * @param string $value the template used to render the body of a menu which is a link. In this template, the token
258
     * `{url}` will be replaced with the corresponding link URL; while `{label}` will be replaced with the link text.
259
     *
260
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
261
     *
262
     * @return $this
263
     */
264 2
    public function linkTemplate(string $value): self
265
    {
266 2
        $this->linkTemplate = $value;
267
268 2
        return $this;
269
    }
270
271
    /**
272
     * @param array $value the HTML attributes for the menu's container tag. The following special options are
273
     * recognized:
274
     *
275
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
276
     *   See also {@see \Yiisoft\Html\Html::tag()}.
277
     *
278
     * @return $this
279
     *
280
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
281
     */
282 1
    public function options(array $value): self
283
    {
284 1
        $this->options = $value;
285
286 1
        return $this;
287
    }
288
289
    /**
290
     * Recursively renders the menu items (without the container tag).
291
     *
292
     * @param array $items the menu items to be rendered recursively
293
     *
294
     * @throws JsonException
295
     *
296
     * @return string the rendering result
297
     */
298 7
    protected function renderItems(array $items): string
299
    {
300 7
        $n = count($items);
301 7
        $lines = [];
302
303 7
        foreach ($items as $i => $item) {
304 7
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
305 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
306 7
            $class = [];
307
308 7
            if ($item['active']) {
309 3
                $class[] = $this->activeCssClass;
310
            }
311
312 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
313
                $class[] = $this->firstItemCssClass;
314
            }
315
316 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
317
                $class[] = $this->lastItemCssClass;
318
            }
319
320 7
            Html::addCssClass($options, $class);
321
322 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

322
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item);
Loading history...
323
324 7
            if (!empty($item['items'])) {
325
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
326
                $menu .= strtr($submenuTemplate, [
327
                    '{items}' => $this->renderItems($item['items']),
328
                ]);
329
            }
330
331 7
            $lines[] = empty($tag) ? $menu : Html::tag($tag, $menu, $options)->encode(false);
332
        }
333
334 7
        return implode("\n", $lines);
335
    }
336
337
    /**
338
     * Renders the content of a menu item.
339
     *
340
     * Note that the container and the sub-menus are not rendered here.
341
     *
342
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
343
     * item.
344
     *
345
     * @return string the rendering result
346
     */
347 7
    protected function renderItem(array $item): string
348
    {
349 7
        if (isset($item['url'])) {
350 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
351
352 7
            return strtr($template, [
353 7
                '{url}' => Html::encode($item['url']),
354 7
                '{label}' => $item['label'],
355
            ]);
356
        }
357
358 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
359
360 2
        return strtr($template, [
361 2
            '{label}' => $item['label'],
362
        ]);
363
    }
364
365
    /**
366
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
367
     *
368
     * @param array $items  the items to be normalized.
369
     * @param bool|null $active whether there is an active child menu item.
370
     *
371
     * @return array the normalized menu items
372
     */
373 7
    protected function normalizeItems(array $items, ?bool &$active): array
374
    {
375 7
        foreach ($items as $i => $item) {
376 7
            if (isset($item['visible']) && !$item['visible']) {
377
                unset($items[$i]);
378
                continue;
379
            }
380
381 7
            if (!isset($item['label'])) {
382
                $item['label'] = '';
383
            }
384
385 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
386 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
387 7
            $hasActiveChild = false;
388
389 7
            if (isset($item['items'])) {
390
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
391
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
392
                    unset($items[$i]['items']);
393
                    if (!isset($item['url'])) {
394
                        unset($items[$i]);
395
                        continue;
396
                    }
397
                }
398
            }
399
400 7
            if (!isset($item['active'])) {
401
                if (
402 6
                    ($this->activateParents && $hasActiveChild)
403 6
                    || ($this->activateItems && $this->isItemActive($item))
404
                ) {
405
                    $active = $items[$i]['active'] = true;
406
                } else {
407 6
                    $items[$i]['active'] = false;
408
                }
409 3
            } elseif ($item['active'] instanceof Closure) {
410 1
                $active = $items[$i]['active'] = call_user_func(
411 1
                    $item['active'],
412
                    $item,
413
                    $hasActiveChild,
414 1
                    $this->isItemActive($item),
415 1
                    $this
416
                );
417 3
            } elseif ($item['active']) {
418 3
                $active = true;
419
            }
420
        }
421
422 7
        return array_values($items);
423
    }
424
425
    /**
426
     * Checks whether a menu item is active.
427
     *
428
     * This is done by checking match that specified in the `url` option of the menu item.
429
     *
430
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
431
     *
432
     * @param array $item the menu item to be checked
433
     * @param bool $active returns the result when the item is active
434
     *
435
     * @return bool whether the menu item is active
436
     */
437 7
    protected function isItemActive(array $item, bool $active = false): bool
438
    {
439
        if (
440 7
            $this->activateItems
441 7
            && $this->currentPath !== '/'
442 7
            && isset($item['url'])
443 7
            && $item['url'] === $this->currentPath
444
        ) {
445
            $active = true;
446
        }
447
448 7
        return $active;
449
    }
450
}
451