Passed
Pull Request — master (#38)
by Evgeniy
02:19
created

Menu   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 422
Duplicated Lines 0 %

Test Coverage

Coverage 98.41%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 125
c 2
b 0
f 0
dl 0
loc 422
ccs 124
cts 126
cp 0.9841
rs 8.5599
wmc 48

18 Methods

Rating   Name   Duplication   Size   Complexity  
A linkTemplate() 0 5 1
A labelTemplate() 0 5 1
A lastItemCssClass() 0 5 1
A activateParents() 0 5 1
A activeCssClass() 0 5 1
A currentPath() 0 5 1
A options() 0 5 1
A withoutEncodeLabels() 0 5 1
A itemOptions() 0 5 1
A showEmptyItems() 0 5 1
A firstItemCssClass() 0 5 1
A items() 0 5 1
A deactivateItems() 0 5 1
B renderItems() 0 37 9
A renderItem() 0 11 2
C normalizeItems() 0 52 17
A run() 0 14 3
A isItemActive() 0 7 4

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

345
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item);
Loading history...
346
347 13
            if (!empty($item['items'])) {
348 2
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
349 2
                $menu .= strtr($submenuTemplate, [
350 2
                    '{items}' => $this->renderItems($item['items']),
351
                ]);
352
            }
353
354 13
            $lines[] = empty($tag) ? $menu : Html::tag($tag, $menu, $options)->encode(false);
355
        }
356
357 13
        return implode("\n", $lines);
358
    }
359
360
    /**
361
     * Renders the content of a menu item.
362
     *
363
     * Note that the container and the sub-menus are not rendered here.
364
     *
365
     * @param array $item The menu item to be rendered. Please refer to {@see items} to see what data might be in the
366
     * item.
367
     *
368
     * @return string The rendering result.
369
     */
370 13
    private function renderItem(array $item): string
371
    {
372 13
        if (isset($item['url'])) {
373 11
            return strtr(ArrayHelper::getValue($item, 'template', $this->linkTemplate), [
374 11
                '{url}' => Html::encode($item['url']),
375 11
                '{label}' => $item['label'],
376
            ]);
377
        }
378
379 5
        return strtr(ArrayHelper::getValue($item, 'template', $this->labelTemplate), [
380 5
            '{label}' => $item['label'],
381
        ]);
382
    }
383
384
    /**
385
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
386
     *
387
     * @param array $items The items to be normalized.
388
     * @param bool|null $active Whether there is an active child menu item.
389
     *
390
     * @return array The normalized menu items.
391
     */
392 15
    private function normalizeItems(array $items, ?bool &$active): array
393
    {
394 15
        foreach ($items as $i => $item) {
395 14
            if (isset($item['visible']) && !$item['visible']) {
396 2
                unset($items[$i]);
397 2
                continue;
398
            }
399
400 14
            if (!isset($item['label'])) {
401 1
                $item['label'] = '';
402
            }
403
404 14
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
405 14
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
406 14
            $hasActiveChild = false;
407
408 14
            if (isset($item['items'])) {
409 4
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
410
411 4
                if (empty($items[$i]['items']) && !$this->showEmptyItems) {
412 1
                    unset($items[$i]['items']);
413
414 1
                    if (!isset($item['url'])) {
415 1
                        unset($items[$i]);
416 1
                        continue;
417
                    }
418
                }
419
            }
420
421 13
            if (!isset($item['active'])) {
422
                if (
423 12
                    ($this->activateParents && $hasActiveChild)
424 12
                    || ($this->activateItems && $this->isItemActive($item))
425
                ) {
426 2
                    $active = $items[$i]['active'] = true;
427
                } else {
428 12
                    $items[$i]['active'] = false;
429
                }
430 4
            } elseif ($item['active'] instanceof Closure) {
431 1
                $active = $items[$i]['active'] = call_user_func(
432 1
                    $item['active'],
433
                    $item,
434
                    $hasActiveChild,
435 1
                    $this->isItemActive($item),
436 1
                    $this,
437
                );
438 4
            } elseif ($item['active']) {
439 4
                $active = true;
440
            }
441
        }
442
443 15
        return array_values($items);
444
    }
445
446
    /**
447
     * Checks whether a menu item is active.
448
     *
449
     * This is done by checking match that specified in the `url` option of the menu item.
450
     *
451
     * @param array $item The menu item to be checked.
452
     *
453
     * @return bool Whether the menu item is active.
454
     */
455 12
    private function isItemActive(array $item): bool
456
    {
457
        return
458 12
            $this->activateItems
459 12
            && $this->currentPath !== '/'
460 12
            && isset($item['url'])
461 12
            && $item['url'] === $this->currentPath
462
        ;
463
    }
464
}
465