Nav   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Test Coverage

Coverage 91.75%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 92
c 5
b 0
f 0
dl 0
loc 323
ccs 89
cts 97
cp 0.9175
rs 9.2
wmc 40

15 Methods

Rating   Name   Duplication   Size   Complexity  
A items() 0 5 1
A renderDropdown() 0 10 1
A activateItems() 0 5 1
B renderItem() 0 45 9
A run() 0 9 2
A encodeLabels() 0 5 1
A dropdownClass() 0 5 1
A currentPath() 0 5 1
A activateParents() 0 5 1
A label() 0 5 1
A params() 0 5 1
A renderItems() 0 13 4
B isChildActive() 0 27 10
A isItemActive() 0 7 5
A options() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Nav 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.

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 Nav, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap4;
6
7
use JsonException;
8
use RuntimeException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
use function is_array;
13
use function is_string;
14
15
/**
16
 * Nav renders a nav HTML component.
17
 *
18
 * For example:
19
 *
20
 * ```php
21
 *    if ($user->getId() !== null) {
22
 *        $menuItems = [
23
 *            [
24
 *                'label' => 'About',
25
 *                'url' => '/about',
26
 *            ],
27
 *            [
28
 *                'label' => 'Contact',
29
 *                'url' => '/contact',
30
 *            ],
31
 *            [
32
 *                'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
33
 *                'url' => '/logout'
34
 *            ],
35
 *        ];
36
 *    } else {
37
 *        $menuItems = [
38
 *            [
39
 *                'label' => 'About',
40
 *                'url' => '/about',
41
 *            ],
42
 *            [
43
 *                'label' => 'Contact',
44
 *                'url' => '/contact',
45
 *            ],
46
 *            [
47
 *                'label' => 'Login',
48
 *                'url' => '/login',
49
 *            ],
50
 *        ];
51
 *    }
52
 *
53
 *    echo Nav::widget()
54
 *        ->currentPath($currentPath)
55
 *        ->items($menuItems)
56
 *        ->options([
57
 *            'class' => 'navbar-nav float-right ml-auto'
58
 *        ]);
59
 *
60
 * Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 3.
61
 * Note: $currentPath it must be injected from each controller to the main controller.
62
 *
63
 * SiteController.php
64
 *
65
 * ```php
66
 *
67
 *    public function index(ServerRequestInterface $request): ResponseInterface
68
 *    {
69
 *        $response = $this->responseFactory->createResponse();
70
 *        $currentPath = $request->getUri()->getPath();
71
 *        $output = $this->render('index', ['currentPath' => $currentPath]);
72
 *        $response->getBody()->write($output);
73
 *
74
 *        return $response;
75
 *    }
76
 * ```
77
 *
78
 * Controller.php
79
 *
80
 * ```php
81
 *    private function renderContent($content, array $parameters = []): string
82
 *    {
83
 *        $user = $this->user->getIdentity();
84
 *        $layout = $this->findLayoutFile($this->layout);
85
 *
86
 *        if ($layout !== null) {
87
 *            return $this->view->renderFile(
88
 *                $layout,
89
 *                    [
90
 *                        'aliases' => $this->aliases,
91
 *                        'content' => $content,
92
 *                        'user' => $user,
93
 *                        'params' => $this->params,
94
 *                        'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
95
 *                    ],
96
 *                $this
97
 *            );
98
 *        }
99
 *
100
 *        return $content;
101
 *    }
102
 * ```
103
 *
104
 * {@see http://getbootstrap.com/components/#dropdowns}
105
 * {@see http://getbootstrap.com/components/#nav}
106
 */
107
class Nav extends Widget
108
{
109
    private string $label = '';
110
    private array $items = [];
111
    private bool $encodeLabels = true;
112
    private bool $activateItems = true;
113
    private bool $activateParents = false;
114
    private ?string $currentPath = null;
115
    private array $params = [];
116
    private string $dropdownClass = Dropdown::class;
117
    private array $options = [];
118
119 19
    protected function run(): string
120
    {
121 19
        if (!isset($this->options['id'])) {
122 9
            $this->options['id'] = "{$this->getId()}-nav";
123
        }
124
125 19
        Html::addCssClass($this->options, ['widget' => 'nav']);
126
127 19
        return $this->renderItems();
128
    }
129
130
    /**
131
     * Renders widget items.
132
     *
133
     * @throws JsonException|RuntimeException
134
     *
135
     * @return string
136
     */
137 19
    public function renderItems(): string
138
    {
139 19
        $items = [];
140
141 19
        foreach ($this->items as $i => $item) {
142 19
            if (isset($item['visible']) && !$item['visible']) {
143 5
                continue;
144
            }
145
146 19
            $items[] = $this->renderItem($item);
147
        }
148
149 19
        return Html::tag('ul', implode("\n", $items), $this->options);
150
    }
151
152
    /**
153
     * Renders a widget's item.
154
     *
155
     * @param array|string $item the item to render.
156
     *
157
     * @throws JsonException|RuntimeException
158
     *
159
     * @return string the rendering result.
160
     */
161 19
    public function renderItem($item): string
162
    {
163 19
        if (is_string($item)) {
164
            return $item;
165
        }
166
167 19
        if (!isset($item['label'])) {
168
            throw new RuntimeException('The "label" option is required.');
169
        }
170
171 19
        $encodeLabel = $item['encode'] ?? $this->encodeLabels;
172 19
        $label = $encodeLabel ? Html::encode($item['label']) : $item['label'];
173 19
        $options = ArrayHelper::getValue($item, 'options', []);
174 19
        $items = ArrayHelper::getValue($item, 'items');
175 19
        $url = ArrayHelper::getValue($item, 'url', '#');
176 19
        $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []);
177 19
        $disabled = ArrayHelper::getValue($item, 'disabled', false);
178 19
        $active = $this->isItemActive($item);
179
180 19
        if (empty($items)) {
181 18
            $items = '';
182
        } else {
183 10
            $linkOptions['data-toggle'] = 'dropdown';
184
185 10
            Html::addCssClass($options, ['widget' => 'dropdown']);
186 10
            Html::addCssClass($linkOptions, ['widget' => 'dropdown-toggle']);
187
188 10
            if (is_array($items)) {
189 10
                $items = $this->isChildActive($items, $active);
190 10
                $items = $this->renderDropdown($items, $item);
191
            }
192
        }
193
194 19
        Html::addCssClass($options, 'nav-item');
195 19
        Html::addCssClass($linkOptions, 'nav-link');
196
197 19
        if ($disabled) {
198 2
            ArrayHelper::setValue($linkOptions, 'tabindex', '-1');
199 2
            ArrayHelper::setValue($linkOptions, 'aria-disabled', 'true');
200 2
            Html::addCssClass($linkOptions, 'disabled');
201 19
        } elseif ($this->activateItems && $active) {
202 12
            Html::addCssClass($linkOptions, 'active');
203
        }
204
205 19
        return Html::tag('li', Html::a($label, $url, $linkOptions) . $items, $options);
206
    }
207
208
    /**
209
     * Renders the given items as a dropdown.
210
     *
211
     * This method is called to create sub-menus.
212
     *
213
     * @param array $items the given items. Please refer to {@see Dropdown::items} for the array structure.
214
     * @param array $parentItem the parent item information. Please refer to {@see items} for the structure of this
215
     * array.
216
     *
217
     * @return string the rendering result.
218
     */
219 10
    protected function renderDropdown(array $items, array $parentItem): string
220
    {
221 10
        $dropdownClass = $this->dropdownClass;
222
223 10
        return $dropdownClass::widget()
224 10
            ->enableClientOptions(false)
225 10
            ->encodeLabels($this->encodeLabels)
226 10
            ->items($items)
227 10
            ->options(ArrayHelper::getValue($parentItem, 'dropdownOptions', []))
228 10
            ->render();
229
    }
230
231
    /**
232
     * Check to see if a child item is active optionally activating the parent.
233
     *
234
     * @param array $items
235
     * @param bool $active should the parent be active too
236
     *
237
     * @return array
238
     *
239
     * {@see items}
240
     */
241 10
    protected function isChildActive(array $items, bool &$active): array
242
    {
243 10
        foreach ($items as $i => $child) {
244 10
            if (is_array($child) && !ArrayHelper::getValue($child, 'visible', true)) {
245 1
                continue;
246
            }
247
248 10
            if ($this->isItemActive($child)) {
249 3
                ArrayHelper::setValue($items[$i], 'active', true);
250 3
                if ($this->activateParents) {
251 1
                    $active = true;
252
                }
253
            }
254
255 10
            if (is_array($child) && ($childItems = ArrayHelper::getValue($child, 'items')) && is_array($childItems)) {
256 1
                $activeParent = false;
257 1
                $items[$i]['items'] = $this->isChildActive($childItems, $activeParent);
258
259 1
                if ($activeParent) {
260 1
                    $items[$i]['options'] ??= [];
261 1
                    Html::addCssClass($items[$i]['options'], 'active');
262 1
                    $active = true;
263
                }
264
            }
265
        }
266
267 10
        return $items;
268
    }
269
270
    /**
271
     * Checks whether a menu item is active.
272
     *
273
     * This is done by checking if {@see currentPath} match that specified in the `url` option of the menu item. When
274
     * the `url` option of a menu item is specified in terms of an array, its first element is treated as the
275
     * currentPath for the item and the rest of the elements are the associated parameters. Only when its currentPath
276
     * and parameters match {@see currentPath}, respectively, will a menu item be considered active.
277
     *
278
     * @param array|string $item the menu item to be checked
279
     *
280
     * @return bool whether the menu item is active
281
     */
282 19
    protected function isItemActive($item): bool
283
    {
284 19
        if (isset($item['active'])) {
285 15
            return ArrayHelper::getValue($item, 'active', false);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type string; however, parameter $array of Yiisoft\Arrays\ArrayHelper::getValue() does only seem to accept array|object, 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

285
            return ArrayHelper::getValue(/** @scrutinizer ignore-type */ $item, 'active', false);
Loading history...
286
        }
287
288 18
        return isset($item['url']) && $this->currentPath !== '/' && $item['url'] === $this->currentPath && $this->activateItems;
289
    }
290
291
    /**
292
     * List of items in the nav widget. Each array element represents a single  menu item which can be either a string
293
     * or an array with the following structure:
294
     *
295
     * - label: string, required, the nav item label.
296
     * - url: optional, the item's URL. Defaults to "#".
297
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
298
     * - linkOptions: array, optional, the HTML attributes of the item's link.
299
     * - options: array, optional, the HTML attributes of the item container (LI).
300
     * - active: bool, optional, whether the item should be on active state or not.
301
     * - dropdownOptions: array, optional, the HTML options that will passed to the {@see Dropdown} widget.
302
     * - items: array|string, optional, the configuration array for creating a {@see Dropdown} widget, or a string
303
     *   representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
304
     * - encode: bool, optional, whether the label will be HTML-encoded. If set, supersedes the $encodeLabels option for
305
     *   only this item.
306
     *
307
     * If a menu item is a string, it will be rendered directly without HTML encoding.
308
     *
309
     * @param array $value
310
     *
311
     * @return $this
312
     */
313 19
    public function items(array $value): self
314
    {
315 19
        $this->items = $value;
316
317 19
        return $this;
318
    }
319
320
    /**
321
     * Whether the nav items labels should be HTML-encoded.
322
     *
323
     * @param bool $value
324
     *
325
     * @return $this
326
     */
327 10
    public function encodeLabels(bool $value): self
328
    {
329 10
        $this->encodeLabels = $value;
330
331 10
        return $this;
332
    }
333
334
    public function label(string $value): self
335
    {
336
        $this->label = $value;
337
338
        return $this;
339
    }
340
341
    /**
342
     * Whether to automatically activate items according to whether their currentPath matches the currently requested.
343
     *
344
     * @param bool $value
345
     *
346
     * @return $this
347
     *
348
     * {@see isItemActive}
349
     */
350 2
    public function activateItems(bool $value): self
351
    {
352 2
        $this->activateItems = $value;
353
354 2
        return $this;
355
    }
356
357
    /**
358
     * Whether to activate parent menu items when one of the corresponding child menu items is active.
359
     *
360
     * @param bool $value
361
     *
362
     * @return $this
363
     */
364 1
    public function activateParents(bool $value): self
365
    {
366 1
        $this->activateParents = $value;
367
368 1
        return $this;
369
    }
370
371
    /**
372
     * Allows you to assign the current path of the url from request controller.
373
     *
374
     * @param string|null $value
375
     *
376
     * @return $this
377
     */
378 2
    public function currentPath(?string $value): self
379
    {
380 2
        $this->currentPath = $value;
381
382 2
        return $this;
383
    }
384
385
    /**
386
     * The parameters used to determine if a menu item is active or not. If not set, it will use `$_GET`.
387
     *
388
     * @param array $value
389
     *
390
     * @return $this
391
     *
392
     * {@see currentPath}
393
     * {@see isItemActive}
394
     */
395
    public function params(array $value): self
396
    {
397
        $this->params = $value;
398
399
        return $this;
400
    }
401
402
    /**
403
     * Name of a class to use for rendering dropdowns within this widget. Defaults to {@see Dropdown}.
404
     *
405
     * @param string $value
406
     *
407
     * @return $this
408
     */
409 10
    public function dropdownClass(string $value): self
410
    {
411 10
        $this->dropdownClass = $value;
412
413 10
        return $this;
414
    }
415
416
    /**
417
     * The HTML attributes for the widget container tag. The following special options are recognized.
418
     *
419
     * {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
420
     *
421
     * @param array $value
422
     *
423
     * @return $this
424
     */
425 11
    public function options(array $value): self
426
    {
427 11
        $this->options = $value;
428
429 11
        return $this;
430
    }
431
}
432