Passed
Pull Request — master (#10)
by Wilmer
01:31
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 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 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
    public 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 Html::tag($tag, $this->renderItems($items), $options);
81
    }
82
83
    /**
84
     * @param bool $value whether to automatically activate items according to whether their route setting matches the
85
     * currently requested route.
86
     *
87
     * @return $this
88
     */
89
    public function activateItems(bool $value): self
90
    {
91
        $this->activateItems = $value;
92
93
        return $this;
94
    }
95
96
    /**
97
     * @param bool $value whether to activate parent menu items when one of the corresponding child menu items is
98
     * active. The activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
99
     *
100
     * @return $this
101
     */
102
    public function activateParents(bool $value): self
103
    {
104
        $this->activateParents = $value;
105
106
        return $this;
107
    }
108
109
    /**
110
     * @param string $value the CSS class to be appended to the active menu item.
111
     *
112
     * @return $this
113
     */
114 2
    public function activeCssClass(string $value): self
115
    {
116 2
        $this->activeCssClass = $value;
117
118 2
        return $this;
119
    }
120
121
    /**
122
     * @param string|null $value allows you to assign the current path of the url from request controller.
123
     *
124
     * @return $this
125
     */
126
    public function currentPath(?string $value): self
127
    {
128
        $this->currentPath = $value;
129
130
        return $this;
131
    }
132
133
    /**
134
     * @param bool $value whether the labels for menu items should be HTML-encoded.
135
     *
136
     * @return $this
137
     */
138 5
    public function encodeLabels(bool $value): self
139
    {
140 5
        $this->encodeLabels = $value;
141
142 5
        return $this;
143
    }
144
145
    /**
146
     * @param string|null the CSS class that will be assigned to the first item in the main menu or each submenu.
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Widgets\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
147
     * Defaults to null, meaning no such CSS class will be assigned.
148
     *
149
     * @return $this
150
     */
151
    public function firstItemCssClass(?string $value): self
152
    {
153
        $this->firstItemCssClass = $value;
154
155
        return $this;
156
    }
157
158
    /**
159
     * @param bool $value whether to hide empty menu items. An empty menu item is one whose `url` option is not set and
160
     * which has no visible child menu items.
161
     *
162
     * @return $this
163
     */
164
    public function hideEmptyItems(bool $value): self
165
    {
166
        $this->hideEmptyItems = $value;
167
168
        return $this;
169
    }
170
171
    /**
172
     * @param array $value list of menu items. Each menu item should be an array of the following structure:
173
     *
174
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
175
     *   HTML-encoded. If the label is not specified, an empty string will be used.
176
     * - encode: bool, optional, whether this item`s label should be HTML-encoded. This param will override global
177
     *   {@see encodeLabels} param.
178
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
179
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
180
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
181
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
182
     * - active: bool or Closure, optional, whether this menu item is in active state (currently selected). When
183
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
184
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
185
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
186
     *   automatically when the current request is triggered by `url`. For more details, please refer to
187
     *   {@see isItemActive()}.
188
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
189
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
190
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
191
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
192
     *   be replaced with the rendered sub-menu items. If this option is not set, {@see submenuTemplate} will be used
193
     *   instead.
194
     * - options: array, optional, the HTML attributes for the menu container tag.
195
     *
196
     * @return $this
197
     */
198 7
    public function items(array $value): self
199
    {
200 7
        $this->items = $value;
201
202 7
        return $this;
203
    }
204
205
    /**
206
     * @param array $value list of HTML attributes shared by all menu {@see items}. If any individual menu item
207
     * specifies its `options`, it will be merged with this property before being used to generate the HTML attributes
208
     * for the menu item tag. The following special options are recognized:
209
     *
210
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
211
     *   See also {@see \Yiisoft\Html\Html::tag()}
212
     *
213
     * @return $this
214
     *
215
     * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
216
     */
217 1
    public function itemOptions(array $value): self
218
    {
219 1
        $this->itemOptions = $value;
220
221 1
        return $this;
222
    }
223
224
    /**
225
     * @param string $value the template used to render the body of a menu which is NOT a link.
226
     *
227
     * In this template, the token `{label}` will be replaced with the label of the menu item.
228
     *
229
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
230
     *
231
     * @return $this
232
     */
233 2
    public function labelTemplate(string $value): self
234
    {
235 2
        $this->labelTemplate = $value;
236
237 2
        return $this;
238
    }
239
240
    /**
241
     * @param string|null $value the CSS class that will be assigned to the last item in the main menu or each submenu.
242
     * Defaults to null, meaning no such CSS class will be assigned.
243
     *
244
     * @return $this
245
     */
246
    public function lastItemCssClass(?string $value): self
247
    {
248
        $this->lastItemCssClass = $value;
249
250
        return $this;
251
    }
252
253
    /**
254
     * @param string $value the template used to render the body of a menu which is a link. In this template, the token
255
     * `{url}` will be replaced with the corresponding link URL; while `{label}` will be replaced with the link text.
256
     *
257
     * This property will be overridden by the `template` option set in individual menu items via {@see items}.
258
     *
259
     * @return $this
260
     */
261 2
    public function linkTemplate(string $value): self
262
    {
263 2
        $this->linkTemplate = $value;
264
265 2
        return $this;
266
    }
267
268
    /**
269
     * @param array $value the HTML attributes for the menu's container tag. The following special options are
270
     * recognized:
271
     *
272
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
273
     *   See also {@see \Yiisoft\Html\Html::tag()}.
274
     *
275
     * @return $this
276
     *
277
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
278
     */
279 1
    public function options(array $value): self
280
    {
281 1
        $this->options = $value;
282
283 1
        return $this;
284
    }
285
286
    /**
287
     * Recursively renders the menu items (without the container tag).
288
     *
289
     * @param array $items the menu items to be rendered recursively
290
     *
291
     * @throws JsonException
292
     *
293
     * @return string the rendering result
294
     */
295 7
    protected function renderItems(array $items): string
296
    {
297 7
        $n = count($items);
298 7
        $lines = [];
299
300 7
        foreach ($items as $i => $item) {
301 7
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
302 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
303 7
            $class = [];
304
305 7
            if ($item['active']) {
306 3
                $class[] = $this->activeCssClass;
307
            }
308
309 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
310
                $class[] = $this->firstItemCssClass;
311
            }
312
313 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
314
                $class[] = $this->lastItemCssClass;
315
            }
316
317 7
            Html::addCssClass($options, $class);
318
319 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

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