Passed
Pull Request — master (#19061)
by
unknown
08:15
created

Menu::normalizeItems()   D

Complexity

Conditions 19
Paths 154

Size

Total Lines 41
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 35.439

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 29
c 1
b 0
f 0
nc 154
nop 2
dl 0
loc 41
ccs 18
cts 28
cp 0.6429
crap 35.439
rs 4.0666

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
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\widgets;
9
10
use Closure;
11
use Yii;
12
use yii\base\Widget;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\Html;
15
use yii\helpers\Url;
16
17
/**
18
 * Menu displays a multi-level menu using nested HTML lists.
19
 *
20
 * The main property of Menu is [[items]], which specifies the possible items in the menu.
21
 * A menu item can contain sub-items which specify the sub-menu under that menu item.
22
 *
23
 * Menu checks the current route and request parameters to toggle certain menu items
24
 * with active state.
25
 *
26
 * Note that Menu only renders the HTML tags about the menu. It does do any styling.
27
 * You are responsible to provide CSS styles to make it look like a real menu.
28
 *
29
 * The following example shows how to use Menu:
30
 *
31
 * ```php
32
 * echo Menu::widget([
33
 *     'items' => [
34
 *         // Important: you need to specify url as 'controller/action',
35
 *         // not just as 'controller' even if default action is used.
36
 *         ['label' => 'Home', 'url' => ['site/index']],
37
 *         // 'Products' menu item will be selected as long as the route is 'product/index'
38
 *         ['label' => 'Products', 'url' => ['product/index'], 'items' => [
39
 *             ['label' => 'New Arrivals', 'url' => ['product/index', 'tag' => 'new']],
40
 *             ['label' => 'Most Popular', 'url' => ['product/index', 'tag' => 'popular']],
41
 *         ]],
42
 *         ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest],
43
 *     ],
44
 * ]);
45
 * ```
46
 *
47
 * @author Qiang Xue <[email protected]>
48
 * @since 2.0
49
 */
50
class Menu extends Widget
51
{
52
    /**
53
     * @var array list of menu items. Each menu item should be an array of the following structure:
54
     *
55
     * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label
56
     *   will be HTML-encoded. If the label is not specified, an empty string will be used.
57
     * - encode: boolean, optional, whether this item`s label should be HTML-encoded. This param will override
58
     *   global [[encodeLabels]] param.
59
     * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Url::to]].
60
     *   When this is set, the actual menu item content will be generated using [[linkTemplate]];
61
     *   otherwise, [[labelTemplate]] will be used.
62
     * - visible: boolean, optional, whether this menu item is visible. Defaults to true.
63
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
64
     * - active: boolean or Closure, optional, whether this menu item is in active state (currently selected).
65
     *   When using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $widget)`.
66
     *   Closure must return `true` if item should be marked as `active`, otherwise - `false`.
67
     *   If a menu item is active, its CSS class will be appended with [[activeCssClass]].
68
     *   If this option is not set, the menu item will be set active automatically when the current request
69
     *   is triggered by `url`. For more details, please refer to [[isItemActive()]].
70
     * - template: string, optional, the template used to render the content of this menu item.
71
     *   The token `{url}` will be replaced by the URL associated with this menu item,
72
     *   and the token `{label}` will be replaced by the label of the menu item.
73
     *   If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead.
74
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus.
75
     *   The token `{items}` will be replaced with the rendered sub-menu items.
76
     *   If this option is not set, [[submenuTemplate]] will be used instead.
77
     * - options: array, optional, the HTML attributes for the menu container tag.
78
     */
79
    public $items = [];
80
    /**
81
     * @var array list of HTML attributes shared by all menu [[items]]. If any individual menu item
82
     * specifies its `options`, it will be merged with this property before being used to generate the HTML
83
     * attributes for the menu item tag. The following special options are recognized:
84
     *
85
     * - tag: string, defaults to "li", the tag name of the item container tags.
86
     *   Set to false to disable container tag.
87
     *   See also [[\yii\helpers\Html::tag()]].
88
     *
89
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
90
     */
91
    public $itemOptions = [];
92
    /**
93
     * @var string the template used to render the body of a menu which is a link.
94
     * In this template, the token `{url}` will be replaced with the corresponding link URL;
95
     * while `{label}` will be replaced with the link text.
96
     * This property will be overridden by the `template` option set in individual menu items via [[items]].
97
     */
98
    public $linkTemplate = '<a href="{url}">{label}</a>';
99
    /**
100
     * @var string the template used to render the body of a menu which is NOT a link.
101
     * In this template, the token `{label}` will be replaced with the label of the menu item.
102
     * This property will be overridden by the `template` option set in individual menu items via [[items]].
103
     */
104
    public $labelTemplate = '{label}';
105
    /**
106
     * @var string the template used to render a list of sub-menus.
107
     * In this template, the token `{items}` will be replaced with the rendered sub-menu items.
108
     */
109
    public $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
110
    /**
111
     * @var bool whether the labels for menu items should be HTML-encoded.
112
     */
113
    public $encodeLabels = true;
114
    /**
115
     * @var string the CSS class to be appended to the active menu item.
116
     */
117
    public $activeCssClass = 'active';
118
    /**
119
     * @var bool whether to automatically activate items according to whether their route setting
120
     * matches the currently requested route.
121
     * @see isItemActive()
122
     */
123
    public $activateItems = true;
124
    /**
125
     * @var bool whether to activate parent menu items when one of the corresponding child menu items is active.
126
     * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]].
127
     */
128
    public $activateParents = false;
129
    /**
130
     * @var bool whether to hide empty menu items. An empty menu item is one whose `url` option is not
131
     * set and which has no visible child menu items.
132
     */
133
    public $hideEmptyItems = true;
134
    /**
135
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
136
     *
137
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
138
     *   See also [[\yii\helpers\Html::tag()]].
139
     *
140
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
141
     */
142
    public $options = [];
143
    /**
144
     * @var string|null the CSS class that will be assigned to the first item in the main menu or each submenu.
145
     * Defaults to null, meaning no such CSS class will be assigned.
146
     */
147
    public $firstItemCssClass;
148
    /**
149
     * @var string|null the CSS class that will be assigned to the last item in the main menu or each submenu.
150
     * Defaults to null, meaning no such CSS class will be assigned.
151
     */
152
    public $lastItemCssClass;
153
    /**
154
     * @var string|null the route used to determine if a menu item is active or not.
155
     * If not set, it will use the route of the current request.
156
     * @see params
157
     * @see isItemActive()
158
     */
159
    public $route;
160
    /**
161
     * @var array|null the parameters used to determine if a menu item is active or not.
162
     * If not set, it will use `$_GET`.
163
     * @see route
164
     * @see isItemActive()
165
     */
166
    public $params;
167
168
169
    /**
170
     * Renders the menu.
171
     */
172 7
    public function run()
173
    {
174 7
        if ($this->route === null && Yii::$app->controller !== null) {
175
            $this->route = Yii::$app->controller->getRoute();
176
        }
177 7
        if ($this->params === null) {
178 1
            $this->params = Yii::$app->request->getQueryParams();
0 ignored issues
show
Bug introduced by
The method getQueryParams() does not exist on yii\console\Request. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

178
            /** @scrutinizer ignore-call */ 
179
            $this->params = Yii::$app->request->getQueryParams();
Loading history...
179
        }
180 7
        $items = $this->normalizeItems($this->items, $hasActiveChild);
181 7
        if (!empty($items)) {
182 7
            $options = $this->options;
183 7
            $tag = ArrayHelper::remove($options, 'tag', 'ul');
184
185 7
            echo Html::tag($tag, $this->renderItems($items), $options);
186
        }
187 7
    }
188
189
    /**
190
     * Recursively renders the menu items (without the container tag).
191
     * @param array $items the menu items to be rendered recursively
192
     * @return string the rendering result
193
     */
194 7
    protected function renderItems($items)
195
    {
196 7
        $n = count($items);
197 7
        $lines = [];
198 7
        foreach ($items as $i => $item) {
199 7
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
200 7
            $tag = ArrayHelper::remove($options, 'tag', 'li');
201 7
            $class = [];
202 7
            if ($item['active']) {
203 3
                $class[] = $this->activeCssClass;
204
            }
205 7
            if ($i === 0 && $this->firstItemCssClass !== null) {
206
                $class[] = $this->firstItemCssClass;
207
            }
208 7
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
209
                $class[] = $this->lastItemCssClass;
210
            }
211 7
            Html::addCssClass($options, $class);
212
213 7
            $menu = $this->renderItem($item);
214 7
            if (!empty($item['items'])) {
215
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
216
                $menu .= strtr($submenuTemplate, [
217
                    '{items}' => $this->renderItems($item['items']),
218
                ]);
219
            }
220 7
            $lines[] = Html::tag($tag, $menu, $options);
221
        }
222
223 7
        return implode("\n", $lines);
224
    }
225
226
    /**
227
     * Renders the content of a menu item.
228
     * Note that the container and the sub-menus are not rendered here.
229
     * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item.
230
     * @return string the rendering result
231
     */
232 7
    protected function renderItem($item)
233
    {
234 7
        if (isset($item['url'])) {
235 7
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
236
237 7
            return strtr($template, [
238 7
                '{url}' => Html::encode(Url::to($item['url'])),
239 7
                '{label}' => $item['label'],
240
            ]);
241
        }
242
243 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
244
245 2
        return strtr($template, [
246 2
            '{label}' => $item['label'],
247
        ]);
248
    }
249
250
    /**
251
     * Normalizes the [[items]] property to remove invisible items and activate certain items.
252
     * @param array $items the items to be normalized.
253
     * @param bool $active whether there is an active child menu item.
254
     * @return array the normalized menu items
255
     */
256 7
    protected function normalizeItems($items, &$active)
257
    {
258 7
        foreach ($items as $i => $item) {
259 7
            if (isset($item['visible']) && !$item['visible']) {
260
                unset($items[$i]);
261
                continue;
262
            }
263 7
            if (!isset($item['label'])) {
264
                $item['label'] = '';
265
            }
266 7
            $encodeLabel = isset($item['encode']) ? $item['encode'] : $this->encodeLabels;
267 7
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
268 7
            $hasActiveChild = false;
269 7
            if (isset($item['items'])) {
270
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
271
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
272
                    unset($items[$i]['items']);
273
                    if (!isset($item['url'])) {
274
                        unset($items[$i]);
275
                        continue;
276
                    }
277
                }
278
            }
279 7
            if (!isset($item['active'])) {
280 6
                if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->activateParents ...is->isItemActive($item), Probably Intended Meaning: $this->activateParents &...s->isItemActive($item))
Loading history...
281
                    $active = $items[$i]['active'] = true;
282
                } else {
283 6
                    $items[$i]['active'] = false;
284
                }
285 3
            } elseif ($item['active'] instanceof Closure) {
286 1
                if (call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this)) {
287 1
                    $active = $items[$i]['active'] = true;
288
                } else {
289 1
                    $items[$i]['active'] = false;
290
                }
291 3
            } elseif ($item['active']) {
292 3
                $active = true;
293
            }
294
        }
295
296 7
        return array_values($items);
297
    }
298
299
    /**
300
     * Checks whether a menu item is active.
301
     * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item.
302
     * When the `url` option of a menu item is specified in terms of an array, its first element is treated
303
     * as the route for the item and the rest of the elements are the associated parameters.
304
     * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item
305
     * be considered active.
306
     * @param array $item the menu item to be checked
307
     * @return bool whether the menu item is active
308
     */
309 7
    protected function isItemActive($item)
310
    {
311 7
        if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
312 1
            $route = Yii::getAlias($item['url'][0]);
313 1
            if (strncmp($route, '/', 1) !== 0 && Yii::$app->controller) {
0 ignored issues
show
Bug introduced by
It seems like $route can also be of type false; however, parameter $string1 of strncmp() does only seem to accept string, 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

313
            if (strncmp(/** @scrutinizer ignore-type */ $route, '/', 1) !== 0 && Yii::$app->controller) {
Loading history...
314
                $route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
0 ignored issues
show
Bug introduced by
Are you sure $route of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
                $route = Yii::$app->controller->module->getUniqueId() . '/' . /** @scrutinizer ignore-type */ $route;
Loading history...
315
            }
316 1
            if (ltrim($route, '/') !== $this->route) {
0 ignored issues
show
Bug introduced by
It seems like $route can also be of type false; however, parameter $string of ltrim() does only seem to accept string, 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

316
            if (ltrim(/** @scrutinizer ignore-type */ $route, '/') !== $this->route) {
Loading history...
317 1
                return false;
318
            }
319
            unset($item['url']['#']);
320
            if (count($item['url']) > 1) {
321
                $params = $item['url'];
322
                unset($params[0]);
323
                foreach ($params as $name => $value) {
324
                    if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) {
325
                        return false;
326
                    }
327
                }
328
            }
329
330
            return true;
331
        }
332
333 6
        return false;
334
    }
335
}
336