Passed
Push — master ( b3e37a...994985 )
by Evgeniy
02:42
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 {@see 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 deactivated items.
64
     *
65
     * Deactivates items according to whether their route setting matches the currently requested route.
66
     *
67
     * @return self
68
     */
69 1
    public function deactivateItems(): self
70
    {
71 1
        $new = clone $this;
72 1
        $new->activateItems = false;
73 1
        return $new;
74
    }
75
76
    /**
77
     * Returns a new instance with 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 1
    public function activateParents(): self
85
    {
86 1
        $new = clone $this;
87 1
        $new->activateParents = true;
88 1
        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 1
    public function currentPath(?string $value): self
113
    {
114 1
        $new = clone $this;
115 1
        $new->currentPath = $value;
116 1
        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 1
    public function firstItemCssClass(?string $value): self
140
    {
141 1
        $new = clone $this;
142 1
        $new->firstItemCssClass = $value;
143 1
        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 1
    public function showEmptyItems(): self
155
    {
156 1
        $new = clone $this;
157 1
        $new->showEmptyItems = true;
158 1
        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 8
    public function items(array $value): self
191
    {
192 8
        $new = clone $this;
193 8
        $new->items = $value;
194 8
        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 1
    public function lastItemCssClass(?string $value): self
245
    {
246 1
        $new = clone $this;
247 1
        $new->lastItemCssClass = $value;
248 1
        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 7
    protected function run(): string
296
    {
297 7
        $items = $this->normalizeItems($this->items, $hasActiveChild);
298
299 7
        if (empty($items)) {
300
            return '';
301
        }
302
303 7
        $options = $this->options;
304 7
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
305
306 7
        return empty($tag)
307 1
            ? $this->renderItems($items)
308 7
            : 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 7
    private function renderItems(array $items): string
322
    {
323 7
        $n = count($items);
324 7
        $lines = [];
325
326 7
        foreach ($items as $i => $item) {
327 7
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
328 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
329 7
            $class = [];
330
331 7
            if ($item['active']) {
332 3
                $class[] = $this->activeCssClass;
333
            }
334
335 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
336
                $class[] = $this->firstItemCssClass;
337
            }
338
339 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
340
                $class[] = $this->lastItemCssClass;
341
            }
342
343 7
            Html::addCssClass($options, $class);
344
345 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

345
            $menu = $this->renderItem(/** @scrutinizer ignore-type */ $item);
Loading history...
346
347 7
            if (!empty($item['items'])) {
348
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
349
                $menu .= strtr($submenuTemplate, [
350
                    '{items}' => $this->renderItems($item['items']),
351
                ]);
352
            }
353
354 7
            $lines[] = empty($tag) ? $menu : Html::tag($tag, $menu, $options)->encode(false);
355
        }
356
357 7
        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 7
    private function renderItem(array $item): string
371
    {
372 7
        if (isset($item['url'])) {
373 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
374
375 7
            return strtr($template, [
376 7
                '{url}' => Html::encode($item['url']),
377 7
                '{label}' => $item['label'],
378
            ]);
379
        }
380
381 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
382
383 2
        return strtr($template, [
384 2
            '{label}' => $item['label'],
385
        ]);
386
    }
387
388
    /**
389
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
390
     *
391
     * @param array $items The items to be normalized.
392
     * @param bool|null $active Whether there is an active child menu item.
393
     *
394
     * @return array The normalized menu items.
395
     */
396 7
    private function normalizeItems(array $items, ?bool &$active): array
397
    {
398 7
        foreach ($items as $i => $item) {
399 7
            if (isset($item['visible']) && !$item['visible']) {
400
                unset($items[$i]);
401
                continue;
402
            }
403
404 7
            if (!isset($item['label'])) {
405
                $item['label'] = '';
406
            }
407
408 7
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
409 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
410 7
            $hasActiveChild = false;
411
412 7
            if (isset($item['items'])) {
413
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
414
                if (empty($items[$i]['items']) && !$this->showEmptyItems) {
415
                    unset($items[$i]['items']);
416
                    if (!isset($item['url'])) {
417
                        unset($items[$i]);
418
                        continue;
419
                    }
420
                }
421
            }
422
423 7
            if (!isset($item['active'])) {
424
                if (
425 6
                    ($this->activateParents && $hasActiveChild)
426 6
                    || ($this->activateItems && $this->isItemActive($item))
427
                ) {
428
                    $active = $items[$i]['active'] = true;
429
                } else {
430 6
                    $items[$i]['active'] = false;
431
                }
432 3
            } elseif ($item['active'] instanceof Closure) {
433 1
                $active = $items[$i]['active'] = call_user_func(
434 1
                    $item['active'],
435
                    $item,
436
                    $hasActiveChild,
437 1
                    $this->isItemActive($item),
438 1
                    $this
439
                );
440 3
            } elseif ($item['active']) {
441 3
                $active = true;
442
            }
443
        }
444
445 7
        return array_values($items);
446
    }
447
448
    /**
449
     * Checks whether a menu item is active.
450
     *
451
     * This is done by checking match that specified in the `url` option of the menu item.
452
     *
453
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
454
     *
455
     * @param array $item The menu item to be checked.
456
     * @param bool $active The result when the item is active.
457
     *
458
     * @return bool Whether the menu item is active
459
     */
460 7
    private function isItemActive(array $item, bool $active = false): bool
461
    {
462
        if (
463 7
            $this->activateItems
464 7
            && $this->currentPath !== '/'
465 7
            && isset($item['url'])
466 7
            && $item['url'] === $this->currentPath
467
        ) {
468
            $active = true;
469
        }
470
471 7
        return $active;
472
    }
473
}
474