Completed
Pull Request — master (#52)
by Wilmer
01:35
created

Menu::currentPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Widget;
5
6
use Closure;
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Html\Html;
9
10
/**
11
 * Menu displays a multi-level menu using nested HTML lists.
12
 *
13
 * The main property of Menu is {@see items}, which specifies the possible items in the menu. A menu item can contain
14
 * sub-items which specify the sub-menu under that menu item.
15
 *
16
 * Menu checks the current route and request parameters to toggle certain menu items with active state.
17
 *
18
 * Note that Menu only renders the HTML tags about the menu. It does not do any styling. You are responsible to provide
19
 * CSS styles to make it look like a real menu.
20
 *
21
 * The following example shows how to use Menu:
22
 *
23
 * ```php
24
 * echo Menu::Widget()
25
 *     ->items([
26
 *         ['label' => 'Login', 'url' => 'site/login', 'visible' => true],
27
 *     ]);
28
 * ```
29
 */
30
class Menu extends Widget
31
{
32
    /**
33
     * @var array list of menu items. Each menu item should be an array of the following structure:
34
     *
35
     * - label: string, optional, specifies the menu item label. When {@see encodeLabels} is true, the label will be
36
     *   HTML-encoded. If the label is not specified, an empty string will be used.
37
     * - encode: boolean, optional, whether this item`s label should be HTML-encoded. This param will override global
38
     *   {@see encodeLabels} param.
39
     * - url: string or array, optional, specifies the URL of the menu item. When this is set, the actual menu item
40
     *   content will be generated using {@see linkTemplate}; otherwise, {@see labelTemplate} will be used.
41
     * - visible: boolean, optional, whether this menu item is visible. Defaults to true.
42
     * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
43
     * - active: boolean or Closure, optional, whether this menu item is in active state (currently selected). When
44
     *   using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $Widget)`. Closure
45
     *   must return `true` if item should be marked as `active`, otherwise - `false`. If a menu item is active, its CSS
46
     *   class will be appended with {@see activeCssClass}. If this option is not set, the menu item will be set active
47
     *   automatically when the current request is triggered by `url`. For more details, please refer to
48
     *   {@see isItemActive()}.
49
     * - template: string, optional, the template used to render the content of this menu item. The token `{url}` will
50
     *   be replaced by the URL associated with this menu item, and the token `{label}` will be replaced by the label
51
     *   of the menu item. If this option is not set, {@see linkTemplate} or {@see labelTemplate} will be used instead.
52
     * - submenuTemplate: string, optional, the template used to render the list of sub-menus. The token `{items}` will
53
     *   be replaced with the rendered sub-menu items. If this option is not set, [[submenuTemplate]] will be used
54
     *   instead.
55
     * - options: array, optional, the HTML attributes for the menu container tag.
56
     */
57
    private $items = [];
58
59
    /**
60
     * @var array list of HTML attributes shared by all menu [[items]]. If any individual menu item specifies its
61
     *            `options`, it will be merged with this property before being used to generate the HTML attributes for
62
     *            the menu item tag. The following special options are recognized:
63
     *
64
     * - tag: string, defaults to "li", the tag name of the item container tags. Set to false to disable container tag.
65
     *   See also {@see \Yiisoft\Html\Html::tag()}
66
     *
67
     * {@see \Yiisoft\Html\Html::renderTagAttributes() for details on how attributes are being rendered}
68
     */
69
    private $itemOptions = [];
70
71
    /**
72
     * @var string the template used to render the body of a menu which is a link. In this template, the token `{url}`
73
     *             will be replaced with the corresponding link URL; while `{label}` will be replaced with the link
74
     *             text. This property will be overridden by the `template` option set in individual menu items via
75
     *             {@see items}.
76
     */
77
    private $linkTemplate = '<a href="{url}">{label}</a>';
78
79
    /**
80
     * @var string the template used to render the body of a menu which is NOT a link.
81
     *             In this template, the token `{label}` will be replaced with the label of the menu item.
82
     *             This property will be overridden by the `template` option set in individual menu items via
83
     *             {@see items}.
84
     */
85
    private $labelTemplate = '{label}';
86
87
    /**
88
     * @var string the template used to render a list of sub-menus.
89
     *             In this template, the token `{items}` will be replaced with the rendered sub-menu items.
90
     */
91
    private $submenuTemplate = "\n<ul>\n{items}\n</ul>\n";
92
93
    /**
94
     * @var bool whether the labels for menu items should be HTML-encoded.
95
     */
96
    private $encodeLabels = true;
97
98
    /**
99
     * @var string the CSS class to be appended to the active menu item.
100
     */
101
    private $activeCssClass = 'active';
102
103
    /**
104
     * @var bool whether to automatically activate items according to whether their route setting matches the currently
105
     *           requested route.
106
     *
107
     * {@see isItemActive()}
108
     */
109
    private $activateItems = true;
110
111
    /**
112
     * @var bool whether to activate parent menu items when one of the corresponding child menu items is active. The
113
     *           activated parent menu items will also have its CSS classes appended with {@see activeCssClass}.
114
     */
115
    private $activateParents = false;
116
117
    /**
118
     * @var string $currentPath Allows you to assign the current path of the url from request controller.
119
     */
120
    private $currentPath;
121
122
    /**
123
     * @var bool whether to hide empty menu items. An empty menu item is one whose `url` option is not set and which has
124
     *           no visible child menu items.
125
     */
126
    private $hideEmptyItems = true;
127
128
    /**
129
     * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
130
     *
131
     * - tag: string, defaults to "ul", the tag name of the item container tags. Set to false to disable container tag.
132
     *   See also {@see \Yiisoft\Html\Html::tag()}.
133
     *
134
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
135
     */
136
    private $options = [];
137
138
    /**
139
     * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. Defaults to
140
     *             null, meaning no such CSS class will be assigned.
141
     */
142
    private $firstItemCssClass;
143
144
    /**
145
     * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. Defaults to
146
     *             null, meaning no such CSS class will be assigned.
147
     */
148
    private $lastItemCssClass;
149
150
    /**
151
     * Renders the menu.
152
     *
153
     * @return string the result of Widget execution to be outputted.
154
     */
155 6
    public function show(): string
156
    {
157 6
        $items = $this->normalizeItems($this->items, $hasActiveChild);
158
159 6
        if (empty($items)) {
160
            return '';
161
        }
162
163 6
        $options = $this->options;
164 6
        $tag = ArrayHelper::remove($options, 'tag', 'ul');
165
166 6
        return Html::tag($tag, $this->renderItems($items), $options);
167
    }
168
169
    /**
170
     * {@see activateItems}
171
     *
172
     * @param boolean $value
173
     *
174
     * @return Widget
175
     */
176
    public function activateItems(bool $value): Widget
177
    {
178
        $this->activateItems = $value;
179
180
        return $this;
181
    }
182
183
    /**
184
     * {@see activateParents}
185
     *
186
     * @param boolean $value
187
     *
188
     * @return Widget
189
     */
190
    public function activateParents(bool $value): Widget
191
    {
192
        $this->activateParents = $value;
193
194
        return $this;
195
    }
196
197
    /**
198
     * {@see activeCssClass}
199
     *
200
     * @param string $value
201
     *
202
     * @return Widget
203
     */
204 2
    public function activeCssClass(string $value): Widget
205
    {
206 2
        $this->activeCssClass = $value;
207
208 2
        return $this;
209
    }
210
211
    /**
212
     * {@see currentPath}
213
     *
214
     * @param boolean $value
215
     *
216
     * @return Widget
217
     */
218
    public function currentPath(string $value): Widget
219
    {
220
        $this->currentPath = $value;
221
222
        return $this;
223
    }
224
225
    /**
226
     * {@see encodeLabels}
227
     *
228
     * @param boolean $value
229
     *
230
     * @return Widget
231
     */
232 4
    public function encodeLabels(bool $value): Widget
233
    {
234 4
        $this->encodeLabels = $value;
235
236 4
        return $this;
237
    }
238
239
    /**
240
     * {@see firstItemCssClass}
241
     *
242
     * @param string $value
243
     *
244
     * @return Widget
245
     */
246
    public function firstItemCssClass(string $value): Widget
247
    {
248
        $this->firstItemCssClass = $value;
249
250
        return $this;
251
    }
252
253
    /**
254
     * {@see hideEmptyItems}
255
     *
256
     * @param boolean $value
257
     *
258
     * @return Widget
259
     */
260
    public function hideEmptyItems(bool $value): Widget
261
    {
262
        $this->hideEmptyItems = $value;
263
264
        return $this;
265
    }
266
267
    /**
268
     * {@see items}
269
     *
270
     * @param array $value
271
     *
272
     * @return Widget
273
     */
274 6
    public function items(array $value): Widget
275
    {
276 6
        $this->items = $value;
277
278 6
        return $this;
279
    }
280
281
    /**
282
     * {@see itemOptions}
283
     *
284
     * @param array $value
285
     *
286
     * @return Widget
287
     */
288 1
    public function itemOptions(array $value): Widget
289
    {
290 1
        $this->itemOptions = $value;
291
292 1
        return $this;
293
    }
294
295
    /**
296
     * {@see labelTemplate}
297
     *
298
     * @param string $value
299
     *
300
     * @return Widget
301
     */
302 2
    public function labelTemplate(string $value): Widget
303
    {
304 2
        $this->labelTemplate = $value;
305
306 2
        return $this;
307
    }
308
309
    /**
310
     * {@see lastItemCssClass}
311
     *
312
     * @param string $value
313
     *
314
     * @return Widget
315
     */
316
    public function lastItemCssClass(string $value): Widget
317
    {
318
        $this->lastItemCssClass = $value;
319
320
        return $this;
321
    }
322
323
    /**
324
     * {@see linkTemplate}
325
     *
326
     * @param string $value
327
     *
328
     * @return Widget
329
     */
330 2
    public function linkTemplate(string $value): Widget
331
    {
332 2
        $this->linkTemplate = $value;
333
334 2
        return $this;
335
    }
336
337
    /**
338
     * {@see options}
339
     *
340
     * @param array $value
341
     *
342
     * @return Widget
343
     */
344 1
    public function options(array $value): Widget
345
    {
346 1
        $this->options = $value;
347
348 1
        return $this;
349
    }
350
351
    /**
352
     * Recursively renders the menu items (without the container tag).
353
     *
354
     * @param array $items the menu items to be rendered recursively
355
     *
356
     * @return string the rendering result
357
     */
358 6
    protected function renderItems(array $items): string
359
    {
360 6
        $n = count($items);
361 6
        $lines = [];
362
363 6
        foreach ($items as $i => $item) {
364 6
            $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', []));
365 6
            $tag = ArrayHelper::remove($options, 'tag', 'li');
366 6
            $class = [];
367
368 6
            if ($item['active']) {
369 3
                $class[] = $this->activeCssClass;
370
            }
371
372 6
            if ($i === 0 && $this->firstItemCssClass !== null) {
373
                $class[] = $this->firstItemCssClass;
374
            }
375
376 6
            if ($i === $n - 1 && $this->lastItemCssClass !== null) {
377
                $class[] = $this->lastItemCssClass;
378
            }
379
380 6
            Html::addCssClass($options, $class);
381
382 6
            $menu = $this->renderItem($item);
383
384 6
            if (!empty($item['items'])) {
385
                $submenuTemplate = ArrayHelper::getValue($item, 'submenuTemplate', $this->submenuTemplate);
386
                $menu .= strtr($submenuTemplate, [
387
                    '{items}' => $this->renderItems($item['items']),
388
                ]);
389
            }
390
391 6
            $lines[] = Html::tag($tag, $menu, $options);
392
        }
393
394 6
        return implode("\n", $lines);
395
    }
396
397
    /**
398
     * Renders the content of a menu item.
399
     * Note that the container and the sub-menus are not rendered here.
400
     *
401
     * @param array $item the menu item to be rendered. Please refer to {@see items} to see what data might be in the
402
     *              item.
403
     *
404
     * @return string the rendering result
405
     */
406 6
    protected function renderItem(array $item): string
407
    {
408 6
        if (isset($item['url'])) {
409 6
            $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
410
411 6
            return strtr($template, [
412 6
                '{url}'   => Html::encode($item['url']),
413 6
                '{label}' => $item['label'],
414
            ]);
415
        }
416
417 2
        $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
418
419 2
        return strtr($template, [
420 2
            '{label}' => $item['label'],
421
        ]);
422
    }
423
424
    /**
425
     * Normalizes the {@see items} property to remove invisible items and activate certain items.
426
     *
427
     * @param array $items  the items to be normalized.
428
     * @param bool|null $active whether there is an active child menu item.
429
     *
430
     * @return array the normalized menu items
431
     */
432 6
    protected function normalizeItems(array $items, ?bool &$active): array
433
    {
434 6
        foreach ($items as $i => $item) {
435 6
            if (isset($item['visible']) && !$item['visible']) {
436
                unset($items[$i]);
437
                continue;
438
            }
439
440 6
            if (!isset($item['label'])) {
441
                $item['label'] = '';
442
            }
443
444 6
            $encodeLabel = $item['encode'] ?? $this->encodeLabels;
445 6
            $items[$i]['label'] = $encodeLabel ? Html::encode($item['label']) : $item['label'];
446 6
            $hasActiveChild = false;
447
448 6
            if (isset($item['items'])) {
449
                $items[$i]['items'] = $this->normalizeItems($item['items'], $hasActiveChild);
450
                if (empty($items[$i]['items']) && $this->hideEmptyItems) {
451
                    unset($items[$i]['items']);
452
                    if (!isset($item['url'])) {
453
                        unset($items[$i]);
454
                        continue;
455
                    }
456
                }
457
            }
458
459 6
            if (!isset($item['active'])) {
460 5
                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...
461
                    $active = $items[$i]['active'] = true;
462
                } else {
463 5
                    $items[$i]['active'] = false;
464
                }
465 3
            } elseif ($item['active'] instanceof Closure) {
466 1
                $active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
467 3
            } elseif ($item['active']) {
468 3
                $active = true;
469
            }
470
        }
471
472 6
        return array_values($items);
473
    }
474
475
    /**
476
     * Checks whether a menu item is active.
477
     *
478
     * This is done by checking match that specified in the `url` option of the menu item.
479
     * Only when 'url' match $_SERVER['REQUEST_URI'] respectively, will a menu item be considered active.
480
     *
481
     * @param array $item the menu item to be checked
482
     *
483
     * @return bool whether the menu item is active
484
     */
485 6
    protected function isItemActive(array $item, bool $active = false): bool
486
    {
487 6
        if (isset($item['url'])) {
488 6
            if (($this->activateItems) && ($this->currentPath !== '/') && ($item['url'] === $this->currentPath)) {
489
                $active = true;
490
            }
491
        }
492
493 6
        return $active;
494
    }
495
496 6
    public function __toString()
497
    {
498 6
        return $this->show();
499
    }
500
}
501