Passed
Pull Request — master (#10)
by Wilmer
01:31
created

Menu   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Test Coverage

Coverage 67.83%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 114
c 1
b 0
f 0
dl 0
loc 406
ccs 78
cts 115
cp 0.6783
rs 8.64
wmc 47

18 Methods

Rating   Name   Duplication   Size   Complexity  
A linkTemplate() 0 5 1
B renderItems() 0 37 8
A labelTemplate() 0 5 1
A renderItem() 0 15 2
A lastItemCssClass() 0 5 1
A activateParents() 0 5 1
A activeCssClass() 0 5 1
C normalizeItems() 0 50 17
A activateItems() 0 5 1
A currentPath() 0 5 1
A hideEmptyItems() 0 5 1
A options() 0 5 1
A itemOptions() 0 5 1
A run() 0 12 2
A isItemActive() 0 12 5
A encodeLabels() 0 5 1
A firstItemCssClass() 0 5 1
A items() 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\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