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

Menu::normalizeItems()   C

Complexity

Conditions 17
Paths 66

Size

Total Lines 52
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 17.0705

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 17
eloc 33
c 2
b 0
f 0
nc 66
nop 2
dl 0
loc 52
ccs 30
cts 32
cp 0.9375
crap 17.0705
rs 5.2166

How to fix   Long Method    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 {@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