Completed
Pull Request — 2.1 (#12704)
by Robert
08:59
created

Menu   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 55.24%

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 5
dl 0
loc 287
ccs 58
cts 105
cp 0.5524
rs 8.3999
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
B run() 0 18 5
D renderItems() 0 37 10
D normalizeItems() 0 36 17
C isItemActive() 0 26 12
A renderItem() 0 17 2

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\widgets;
9
10
use Yii;
11
use yii\base\Widget;
12
use yii\helpers\ArrayHelper;
13
use yii\helpers\Url;
14
use yii\helpers\Html;
15
16
/**
17
 * Menu displays a multi-level menu using nested HTML lists.
18
 *
19
 * The main property of Menu is [[items]], which specifies the possible items in the menu.
20
 * A menu item can contain sub-items which specify the sub-menu under that menu item.
21
 *
22
 * Menu checks the current route and request parameters to toggle certain menu items
23
 * with active state.
24
 *
25
 * Note that Menu only renders the HTML tags about the menu. It does do any styling.
26
 * You are responsible to provide CSS styles to make it look like a real menu.
27
 *
28
 * The following example shows how to use Menu:
29
 *
30
 * ```php
31
 * echo Menu::widget([
32
 *     'items' => [
33
 *         // Important: you need to specify url as 'controller/action',
34
 *         // not just as 'controller' even if default action is used.
35
 *         ['label' => 'Home', 'url' => ['site/index']],
36
 *         // 'Products' menu item will be selected as long as the route is 'product/index'
37
 *         ['label' => 'Products', 'url' => ['product/index'], 'items' => [
38
 *             ['label' => 'New Arrivals', 'url' => ['product/index', 'tag' => 'new']],
39
 *             ['label' => 'Most Popular', 'url' => ['product/index', 'tag' => 'popular']],
40
 *         ]],
41
 *         ['label' => 'Login', 'url' => ['site/login'], 'visible' => Yii::$app->user->isGuest],
42
 *     ],
43
 * ]);
44
 * ```
45
 *
46
 * @author Qiang Xue <[email protected]>
47
 * @since 2.0
48
 */
49
class Menu extends Widget
50
{
51
    /**
52
     * @var array list of menu items. Each menu item should be an array of the following structure:
53
     *
54
     * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label
55
     *   will be HTML-encoded. If the label is not specified, an empty string will be used.
56
     * - encode: boolean, optional, whether this item`s label should be HTML-encoded. This param will override
57
     *   global [[encodeLabels]] param.
58
     * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Url::to]].
59
     *   When this is set, the actual menu item content will be generated using [[linkTemplate]];
60
     *   otherwise, [[labelTemplate]] will be used.
61
     * - visible: boolean, optional, whether this menu item is visible. Defaults to true.
62
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
63
     * - active: boolean, optional, whether this menu item is in active state (currently selected).
64
     *   If a menu item is active, its CSS class will be appended with [[activeCssClass]].
65
     *   If this option is not set, the menu item will be set active automatically when the current request
66
     *   is triggered by `url`. For more details, please refer to [[isItemActive()]].
67
     * - template: string, optional, the template used to render the content of this menu item.
68
     *   The token `{url}` will be replaced by the URL associated with this menu item,
69
     *   and the token `{label}` will be replaced by the label of the menu item.
70
     *   If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead.
71
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus.
72
     *   The token `{items}` will be replaced with the rendered sub-menu items.
73
     *   If this option is not set, [[submenuTemplate]] will be used instead.
74
     * - options: array, optional, the HTML attributes for the menu container tag.
75
     */
76
    public $items = [];
77
    /**
78
     * @var array list of HTML attributes shared by all menu [[items]]. If any individual menu item
79
     * specifies its `options`, it will be merged with this property before being used to generate the HTML
80
     * attributes for the menu item tag. The following special options are recognized:
81
     *
82
     * - tag: string, defaults to "li", the tag name of the item container tags.
83
     *   Set to false to disable container tag.
84
     *   See also [[\yii\helpers\Html::tag()]].
85
     *
86
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
87
     */
88
    public $itemOptions = [];
89
    /**
90
     * @var string the template used to render the body of a menu which is a link.
91
     * In this template, the token `{url}` will be replaced with the corresponding link URL;
92
     * while `{label}` will be replaced with the link text.
93
     * This property will be overridden by the `template` option set in individual menu items via [[items]].
94
     */
95
    public $linkTemplate = '<a href="{url}">{label}</a>';
96
    /**
97
     * @var string the template used to render the body of a menu which is NOT a link.
98
     * In this template, the token `{label}` will be replaced with the label of the menu item.
99
     * This property will be overridden by the `template` option set in individual menu items via [[items]].
100
     */
101
    public $labelTemplate = '{label}';
102
    /**
103
     * @var string the template used to render a list of sub-menus.
104
     * In this template, the token `{items}` will be replaced with the rendered sub-menu items.
105
     */
106
    public $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
107
    /**
108
     * @var boolean whether the labels for menu items should be HTML-encoded.
109
     */
110
    public $encodeLabels = true;
111
    /**
112
     * @var string the CSS class to be appended to the active menu item.
113
     */
114
    public $activeCssClass = 'active';
115
    /**
116
     * @var boolean whether to automatically activate items according to whether their route setting
117
     * matches the currently requested route.
118
     * @see isItemActive()
119
     */
120
    public $activateItems = true;
121
    /**
122
     * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active.
123
     * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]].
124
     */
125
    public $activateParents = false;
126
    /**
127
     * @var boolean whether to hide empty menu items. An empty menu item is one whose `url` option is not
128
     * set and which has no visible child menu items.
129
     */
130
    public $hideEmptyItems = true;
131
    /**
132
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
133
     *
134
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
135
     *   See also [[\yii\helpers\Html::tag()]].
136
     *
137
     * @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
138
     */
139
    public $options = [];
140
    /**
141
     * @var string the CSS class that will be assigned to the first item in the main menu or each submenu.
142
     * Defaults to null, meaning no such CSS class will be assigned.
143
     */
144
    public $firstItemCssClass;
145
    /**
146
     * @var string the CSS class that will be assigned to the last item in the main menu or each submenu.
147
     * Defaults to null, meaning no such CSS class will be assigned.
148
     */
149
    public $lastItemCssClass;
150
    /**
151
     * @var string the route used to determine if a menu item is active or not.
152
     * If not set, it will use the route of the current request.
153
     * @see params
154
     * @see isItemActive()
155
     */
156
    public $route;
157
    /**
158
     * @var array the parameters used to determine if a menu item is active or not.
159
     * If not set, it will use `$_GET`.
160
     * @see route
161
     * @see isItemActive()
162
     */
163
    public $params;
164
165
166
    /**
167
     * Renders the menu.
168
     * @return string the result of widget execution to be outputted.
169
     */
170 4
    public function run()
171
    {
172 4
        if ($this->route === null && Yii::$app->controller !== null) {
173
            $this->route = Yii::$app->controller->getRoute();
174
        }
175 4
        if ($this->params === null) {
176 1
            $this->params = Yii::$app->request->getQueryParams();
177 1
        }
178 4
        $items = $this->normalizeItems($this->items, $hasActiveChild);
179 4
        if (empty($items)) {
180
            return '';
181
        }
182
183 4
        $options = $this->options;
184 4
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
185
186 4
        return Html::tag($tag, $this->renderItems($items), $options);
187
    }
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 4
    protected function renderItems($items)
195
    {
196 4
        $n = count($items);
197 4
        $lines = [];
198 4
        foreach ($items as $i => $item) {
199 4
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
200 4
            $tag = ArrayHelper::remove($options, 'tag', 'li');
201 4
            $class = [];
202 4
            if ($item['active']) {
203
                $class[] = $this->activeCssClass;
204
            }
205 4
            if ($i === 0 && $this->firstItemCssClass !== null) {
206
                $class[] = $this->firstItemCssClass;
207
            }
208 4
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
209
                $class[] = $this->lastItemCssClass;
210
            }
211 4
            if (!empty($class)) {
212
                if (empty($options['class'])) {
213
                    $options['class'] = implode(' ', $class);
214
                } else {
215
                    $options['class'] .= ' ' . implode(' ', $class);
216
                }
217
            }
218
219 4
            $menu = $this->renderItem($item);
220 4
            if (!empty($item['items'])) {
221
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
222
                $menu .= strtr($submenuTemplate, [
223
                    '{items}' => $this->renderItems($item['items']),
224
                ]);
225
            }
226 4
            $lines[] = Html::tag($tag, $menu, $options);
227 4
        }
228
229 4
        return implode("\n", $lines);
230
    }
231
232
    /**
233
     * Renders the content of a menu item.
234
     * Note that the container and the sub-menus are not rendered here.
235
     * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item.
236
     * @return string the rendering result
237
     */
238 4
    protected function renderItem($item)
239
    {
240 4
        if (isset($item['url'])) {
241 4
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
242
243 4
            return strtr($template, [
244 4
                '{url}' => Html::encode(Url::to($item['url'])),
245 4
                '{label}' => $item['label'],
246 4
            ]);
247
        } else {
248 1
            $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
249
250 1
            return strtr($template, [
251 1
                '{label}' => $item['label'],
252 1
            ]);
253
        }
254
    }
255
256
    /**
257
     * Normalizes the [[items]] property to remove invisible items and activate certain items.
258
     * @param array $items the items to be normalized.
259
     * @param boolean $active whether there is an active child menu item.
260
     * @return array the normalized menu items
261
     */
262 4
    protected function normalizeItems($items, &$active)
263
    {
264 4
        foreach ($items as $i => $item) {
265 4
            if (isset($item['visible']) && !$item['visible']) {
266
                unset($items[$i]);
267
                continue;
268
            }
269 4
            if (!isset($item['label'])) {
270
                $item['label'] = '';
271
            }
272 4
            $encodeLabel = isset($item['encode']) ? $item['encode'] : $this->encodeLabels;
273 4
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
274 4
            $hasActiveChild = false;
275 4
            if (isset($item['items'])) {
276
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
277
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
278
                    unset($items[$i]['items']);
279
                    if (!isset($item['url'])) {
280
                        unset($items[$i]);
281
                        continue;
282
                    }
283
                }
284
            }
285 4
            if (!isset($item['active'])) {
286 4
                if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) {
287
                    $active = $items[$i]['active'] = true;
288
                } else {
289 4
                    $items[$i]['active'] = false;
290
                }
291 4
            } elseif ($item['active']) {
292
                $active = true;
293
            }
294 4
        }
295
296 4
        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 boolean whether the menu item is active
308
     */
309 4
    protected function isItemActive($item)
310
    {
311 4
        if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
312 1
            $route = Yii::getAlias($item['url'][0]);
313 1
            if ($route[0] !== '/' && Yii::$app->controller) {
314
                $route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
315
            }
316 1
            if (ltrim($route, '/') !== $this->route) {
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 3
        return false;
334
    }
335
}
336