Issues (910)

framework/widgets/Menu.php (1 issue)

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 not 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 11
    public function run()
173
    {
174 11
        if ($this->route === null && Yii::$app->controller !== null) {
175
            $this->route = Yii::$app->controller->getRoute();
176
        }
177 11
        if ($this->params === null) {
178 1
            $this->params = Yii::$app->request->getQueryParams();
179
        }
180 11
        $items = $this->normalizeItems($this->items, $hasActiveChild);
181 11
        if (!empty($items)) {
182 11
            $options = $this->options;
183 11
            $tag = ArrayHelper::remove($options, 'tag', 'ul');
184
185 11
            echo Html::tag($tag, $this->renderItems($items), $options);
186
        }
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 11
    protected function renderItems($items)
195
    {
196 11
        $n = count($items);
197 11
        $lines = [];
198 11
        foreach ($items as $i => $item) {
199 11
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
200 11
            $tag = ArrayHelper::remove($options, 'tag', 'li');
201 11
            $class = [];
202 11
            if ($item['active']) {
203 7
                $class[] = $this->activeCssClass;
204
            }
205 11
            if ($i === 0 && $this->firstItemCssClass !== null) {
206
                $class[] = $this->firstItemCssClass;
207
            }
208 11
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
209
                $class[] = $this->lastItemCssClass;
210
            }
211 11
            Html::addCssClass($options, $class);
212
213 11
            $menu = $this->renderItem($item);
214 11
            if (!empty($item['items'])) {
215 2
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
216 2
                $menu .= strtr($submenuTemplate, [
217 2
                    '{items}' => $this->renderItems($item['items']),
218 2
                ]);
219
            }
220 11
            $lines[] = Html::tag($tag, $menu, $options);
221
        }
222
223 11
        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 11
    protected function renderItem($item)
233
    {
234 11
        if (isset($item['url'])) {
235 11
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
236
237 11
            return strtr($template, [
238 11
                '{url}' => Html::encode(Url::to($item['url'])),
239 11
                '{label}' => $item['label'],
240 11
            ]);
241
        }
242
243 4
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
244
245 4
        return strtr($template, [
246 4
            '{label}' => $item['label'],
247 4
        ]);
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 11
    protected function normalizeItems($items, &$active)
257
    {
258 11
        foreach ($items as $i => $item) {
259 11
            if (isset($item['visible']) && !$item['visible']) {
260
                unset($items[$i]);
261
                continue;
262
            }
263 11
            if (!isset($item['label'])) {
264
                $item['label'] = '';
265
            }
266 11
            $encodeLabel = isset($item['encode']) ? $item['encode'] : $this->encodeLabels;
267 11
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
268 11
            $hasActiveChild = false;
269 11
            if (isset($item['items'])) {
270 2
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
271 2
                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 11
            if (!isset($item['active'])) {
280 10
                if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) {
281 3
                    $active = $items[$i]['active'] = true;
282
                } else {
283 10
                    $items[$i]['active'] = false;
284
                }
285 6
            } elseif ($item['active'] instanceof Closure) {
286 4
                if (call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this)) {
287 3
                    $active = $items[$i]['active'] = true;
288
                } else {
289 4
                    $items[$i]['active'] = false;
290
                }
291 3
            } elseif ($item['active']) {
292 3
                $active = true;
293
            }
294
        }
295
296 11
        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 11
    protected function isItemActive($item)
310
    {
311 11
        if (isset($item['url']) && is_array($item['url']) && isset($item['url'][0])) {
312 3
            $route = Yii::getAlias($item['url'][0]);
313 3
            if (strncmp($route, '/', 1) !== 0 && Yii::$app->controller) {
0 ignored issues
show
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;
315
            }
316 3
            if (ltrim($route, '/') !== $this->route) {
317 2
                return false;
318
            }
319 2
            unset($item['url']['#']);
320 2
            if (count($item['url']) > 1) {
321 1
                $params = $item['url'];
322 1
                unset($params[0]);
323 1
                foreach ($params as $name => $value) {
324 1
                    if ($value !== null && (!isset($this->params[$name]) || $this->params[$name] != $value)) {
325
                        return false;
326
                    }
327
                }
328
            }
329
330 2
            return true;
331
        }
332
333 9
        return false;
334
    }
335
}
336