Completed
Push — cache-closure ( 7d9053...380edd )
by Dmitry
35:54
created

Menu::isItemActive()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 48.2817

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
ccs 7
cts 19
cp 0.3684
rs 5.1612
cc 12
eloc 16
nc 9
nop 1
crap 48.2817

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 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 Closure;
11
use Yii;
12
use yii\base\Widget;
13
use yii\helpers\ArrayHelper;
14
use yii\helpers\Url;
15
use yii\helpers\Html;
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 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 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 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 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 5
    public function run()
173
    {
174 5
        if ($this->route === null && Yii::$app->controller !== null) {
175
            $this->route = Yii::$app->controller->getRoute();
176
        }
177 5
        if ($this->params === null) {
178 1
            $this->params = Yii::$app->request->getQueryParams();
179 1
        }
180 5
        $items = $this->normalizeItems($this->items, $hasActiveChild);
181 5
        if (!empty($items)) {
182 5
            $options = $this->options;
183 5
            $tag = ArrayHelper::remove($options, 'tag', 'ul');
184
185 5
            echo Html::tag($tag, $this->renderItems($items), $options);
186 5
        }
187 5
    }
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 5
    protected function renderItems($items)
195
    {
196 5
        $n = count($items);
197 5
        $lines = [];
198 5
        foreach ($items as $i => $item) {
199 5
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
200 5
            $tag = ArrayHelper::remove($options, 'tag', 'li');
201 5
            $class = [];
202 5
            if ($item['active']) {
203 1
                $class[] = $this->activeCssClass;
204 1
            }
205 5
            if ($i === 0 && $this->firstItemCssClass !== null) {
206
                $class[] = $this->firstItemCssClass;
207
            }
208 5
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
209
                $class[] = $this->lastItemCssClass;
210
            }
211 5
            if (!empty($class)) {
212 1
                if (empty($options['class'])) {
213 1
                    $options['class'] = implode(' ', $class);
214 1
                } else {
215
                    $options['class'] .= ' ' . implode(' ', $class);
216
                }
217 1
            }
218
219 5
            $menu = $this->renderItem($item);
220 5
            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 5
            $lines[] = Html::tag($tag, $menu, $options);
227 5
        }
228
229 5
        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 5
    protected function renderItem($item)
239
    {
240 5
        if (isset($item['url'])) {
241 5
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
242
243 5
            return strtr($template, [
244 5
                '{url}' => Html::encode(Url::to($item['url'])),
245 5
                '{label}' => $item['label'],
246 5
            ]);
247
        } else {
248 2
            $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
249
250 2
            return strtr($template, [
251 2
                '{label}' => $item['label'],
252 2
            ]);
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 bool $active whether there is an active child menu item.
260
     * @return array the normalized menu items
261
     */
262 5
    protected function normalizeItems($items, &$active)
263
    {
264 5
        foreach ($items as $i => $item) {
265 5
            if (isset($item['visible']) && !$item['visible']) {
266
                unset($items[$i]);
267
                continue;
268
            }
269 5
            if (!isset($item['label'])) {
270
                $item['label'] = '';
271
            }
272 5
            $encodeLabel = isset($item['encode']) ? $item['encode'] : $this->encodeLabels;
273 5
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
274 5
            $hasActiveChild = false;
275 5
            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 5
            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 5
            } elseif ($item['active'] instanceof Closure) {
292 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
293 1
            } elseif ($item['active']) {
294 1
                $active = true;
295 1
            }
296 5
        }
297
298 5
        return array_values($items);
299
    }
300
301
    /**
302
     * Checks whether a menu item is active.
303
     * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item.
304
     * When the `url` option of a menu item is specified in terms of an array, its first element is treated
305
     * as the route for the item and the rest of the elements are the associated parameters.
306
     * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item
307
     * be considered active.
308
     * @param array $item the menu item to be checked
309
     * @return bool whether the menu item is active
310
     */
311 5
    protected function isItemActive($item)
312
    {
313 5
        if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
314 1
            $route = Yii::getAlias($item['url'][0]);
315 1
            if ($route[0] !== '/' && Yii::$app->controller) {
316
                $route = Yii::$app->controller->module->getUniqueId() . '/' . $route;
317
            }
318 1
            if (ltrim($route, '/') !== $this->route) {
319 1
                return false;
320
            }
321
            unset($item['url']['#']);
322
            if (count($item['url']) > 1) {
323
                $params = $item['url'];
324
                unset($params[0]);
325
                foreach ($params as $name => $value) {
326
                    if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) {
327
                        return false;
328
                    }
329
                }
330
            }
331
332
            return true;
333
        }
334
335 4
        return false;
336
    }
337
}
338