Passed
Push — master ( 1b2ad2...d912e9 )
by Alexander
02:41
created

NavBar   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 97.18%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 150
c 1
b 0
f 0
dl 0
loc 444
ccs 138
cts 142
cp 0.9718
rs 9.1199
wmc 41

22 Methods

Rating   Name   Duplication   Size   Complexity  
A togglerContent() 0 6 1
A brandImage() 0 6 1
B begin() 0 54 10
A run() 0 20 4
A togglerOptions() 0 6 1
A brandOptions() 0 6 1
A dark() 0 3 1
A expandSize() 0 6 1
A theme() 0 6 1
A options() 0 6 1
A getId() 0 3 1
A brandText() 0 6 1
A brandImageAttributes() 0 6 1
A withoutRenderInnerContainer() 0 6 1
A collapseOptions() 0 6 1
A renderBrand() 0 30 6
A renderToggleButton() 0 28 3
A screenReaderToggleText() 0 6 1
A brandUrl() 0 6 1
A offcanvas() 0 6 1
A light() 0 3 1
A innerContainerOptions() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like NavBar 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 NavBar, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use Stringable;
8
use JsonException;
9
use Yiisoft\Arrays\ArrayHelper;
10
use Yiisoft\Html\Html;
11
12
/**
13
 * NavBar renders a navbar HTML component.
14
 *
15
 * Any content enclosed between the {@see begin()} and {@see end()} calls of NavBar is treated as the content of the
16
 * navbar. You may use widgets such as {@see Nav} or {@see \Yiisoft\Widget\Menu} to build up such content. For example,
17
 *
18
 * ```php
19
 *    if ($user->getId() !== null) {
20
 *        $menuItems = [
21
 *            [
22
 *                'label' => 'About',
23
 *                'url' => '/about',
24
 *            ],
25
 *            [
26
 *                'label' => 'Contact',
27
 *                'url' => '/contact',
28
 *            ],
29
 *            [
30
 *                'label' => 'Logout' . ' ' . '(' . $user->getUsername() . ')',
31
 *                'url' => '/logout'
32
 *            ],
33
 *        ];
34
 *    } else {
35
 *        $menuItems = [
36
 *            [
37
 *                'label' => 'About',
38
 *                'url' => '/about',
39
 *            ],
40
 *            [
41
 *                'label' => 'Contact',
42
 *                'url' => '/contact',
43
 *            ],
44
 *            [
45
 *                'label' => 'Login',
46
 *                'url' => '/login',
47
 *            ],
48
 *        ];
49
 *    }
50
 *
51
 *    <?= NavBar::widget()
52
 *        ->brandText('My Application Basic')
53
 *        ->brandUrl('/')
54
 *        ->options([
55
 *            'class' => 'navbar navbar-dark bg-dark navbar-expand-lg text-white',
56
 *        ])
57
 *        ->begin();
58
 *
59
 *        echo Nav::widget()
60
 *            ->currentPath($currentPath)
61
 *            ->items($menuItems)
62
 *            ->options([
63
 *                'class' => 'navbar-nav float-right ml-auto'
64
 *            ]);
65
 *
66
 *    echo NavBar::end(); ?>
67
 * ```
68
 * Note: $currentPath it must be injected from each controller to the main controller.
69
 *
70
 * SiteController.php
71
 *
72
 * ```php
73
 *
74
 *    public function index(ServerRequestInterface $request): ResponseInterface
75
 *    {
76
 *        $response = $this->responseFactory->createResponse();
77
 *        $currentPath = $request
78
 *            ->getUri()
79
 *            ->getPath();
80
 *        $output = $this->render('index', ['currentPath' => $currentPath]);
81
 *        $response
82
 *            ->getBody()
83
 *            ->write($output);
84
 *
85
 *        return $response;
86
 *    }
87
 * ```
88
 *
89
 * Controller.php
90
 *
91
 * ```php
92
 *    private function renderContent($content, array $parameters = []): string
93
 *    {
94
 *        $user = $this->user->getIdentity();
95
 *        $layout = $this->findLayoutFile($this->layout);
96
 *
97
 *        if ($layout !== null) {
98
 *            return $this->view->renderFile(
99
 *                $layout,
100
 *                    [
101
 *                        'aliases' => $this->aliases,
102
 *                        'content' => $content,
103
 *                        'user' => $user,
104
 *                        'params' => $this->params,
105
 *                        'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
106
 *                    ],
107
 *                $this
108
 *            );
109
 *        }
110
 *
111
 *        return $content;
112
 *    }
113
 * ```
114
 */
115
final class NavBar extends Widget
116
{
117
    public const EXPAND_SM = 'navbar-expand-sm';
118
    public const EXPAND_MD = 'navbar-expand-md';
119
    public const EXPAND_LG = 'navbar-expand-lg';
120
    public const EXPAND_XL = 'navbar-expand-xl';
121
    public const EXPAND_XXL = 'navbar-expand-xxl';
122
123
    public const THEME_LIGHT = 'navbar-light';
124
    public const THEME_DARK = 'navbar-dark';
125
126
    private array $collapseOptions = [];
127
    private ?string $brandText = null;
128
    private ?string $brandImage = null;
129
    private array $brandImageAttributes = [];
130
    private ?string $brandUrl = '/';
131
    private array $brandOptions = [];
132
    private string $screenReaderToggleText = 'Toggle navigation';
133
    private string $togglerContent = '<span class="navbar-toggler-icon"></span>';
134
    private array $togglerOptions = [];
135
    private bool $renderInnerContainer = true;
136
    private array $innerContainerOptions = [];
137
    private array $options = [];
138
    private bool $encodeTags = false;
139
    private ?string $expandSize = self::EXPAND_LG;
140
    private ?string $theme = self::THEME_LIGHT;
141
    private ?Offcanvas $offcanvas = null;
142
143 20
    public function getId(?string $suffix = '-navbar'): ?string
144
    {
145 20
        return $this->options['id'] ?? parent::getId($suffix);
146
    }
147
148 20
    public function begin(): string
149
    {
150
        /** Run Offcanvas::begin before NavBar parent::begin for right stack order */
151 20
        $offcanvas = $this->offcanvas ? $this->offcanvas->begin() : null;
152
153 20
        parent::begin();
154
155 20
        $options = $this->options;
156 20
        $options['id'] = $this->getId();
157 20
        $navTag = ArrayHelper::remove($options, 'tag', 'nav');
158 20
        $classNames = ['widget' => 'navbar'];
159
160 20
        if ($this->expandSize) {
161 17
            $classNames['size'] = $this->expandSize;
162
        }
163
164 20
        if ($this->theme) {
165 19
            $classNames['theme'] = $this->theme;
166
        }
167
168 20
        Html::addCssClass($options, $classNames);
169
170 20
        if (!isset($this->innerContainerOptions['class'])) {
171 18
            Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']);
172
        }
173
174 20
        $htmlStart = Html::openTag($navTag, $options);
175
176 20
        if ($this->renderInnerContainer) {
177 19
            $htmlStart .= Html::openTag('div', $this->innerContainerOptions);
178
        }
179
180 20
        $htmlStart .= $this->renderBrand();
181
182 20
        if ($offcanvas) {
183 2
            $htmlStart .= $this->renderToggleButton($this->offcanvas->getId());
0 ignored issues
show
Bug introduced by
The method getId() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
            $htmlStart .= $this->renderToggleButton($this->offcanvas->/** @scrutinizer ignore-call */ getId());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
184 2
            $htmlStart .= $offcanvas;
185 18
        } elseif ($this->expandSize) {
186 16
            $collapseOptions = $this->collapseOptions;
187 16
            $collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div');
188
189 16
            if (!isset($collapseOptions['id'])) {
190 16
                $collapseOptions['id'] = $options['id'] . '-collapse';
191
            }
192
193 16
            Html::addCssClass($collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']);
194
195 16
            $htmlStart .= $this->renderToggleButton($collapseOptions['id']);
196 16
            $htmlStart .= Html::openTag($collapseTag, $collapseOptions);
197 2
        } elseif ($this->togglerOptions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->togglerOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
198 1
            $htmlStart .= $this->renderToggleButton(null);
199
        }
200
201 20
        return $htmlStart;
202
    }
203
204 20
    protected function run(): string
205
    {
206 20
        $htmlRun = '';
207
208 20
        if ($this->offcanvas) {
209 2
            $htmlRun = $this->offcanvas::end();
210 18
        } elseif ($this->expandSize) {
211 16
            $tag = ArrayHelper::getValue($this->collapseOptions, 'tag', 'div');
212 16
            $htmlRun = Html::closeTag($tag);
213
        }
214
215 20
        if ($this->renderInnerContainer) {
216 19
            $htmlRun .= Html::closeTag('div');
217
        }
218
219 20
        $tag = ArrayHelper::getValue($this->options, 'tag', 'nav');
220
221 20
        $htmlRun .= Html::closeTag($tag);
222
223 20
        return $htmlRun;
224
    }
225
226
    /**
227
     * Set size before then content will be expanded
228
     *
229
     * @param string|null $size
230
     *
231
     * @return self
232
     */
233 5
    public function expandSize(?string $size): self
234
    {
235 5
        $new = clone $this;
236 5
        $new->expandSize = $size;
237
238 5
        return $new;
239
    }
240
241
    /**
242
     * Set color theme for NavBar
243
     *
244
     * @param string|null $theme
245
     *
246
     * @return self
247
     */
248 2
    public function theme(?string $theme): self
249
    {
250 2
        $new = clone $this;
251 2
        $new->theme = $theme;
252
253 2
        return $new;
254
    }
255
256
    /**
257
     * Short method for light navbar theme
258
     *
259
     * @return self
260
     */
261
    public function light(): self
262
    {
263
        return $this->theme(self::THEME_LIGHT);
264
    }
265
266
    /**
267
     * Short method for dark navbar theme
268
     *
269
     * @return self
270
     */
271
    public function dark(): self
272
    {
273
        return $this->theme(self::THEME_DARK);
274
    }
275
276
    /**
277
     * The HTML attributes for the container tag. The following special options are recognized.
278
     *
279
     * @param array $value
280
     *
281
     * @return self
282
     *
283
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
284
     */
285 1
    public function collapseOptions(array $value): self
286
    {
287 1
        $new = clone $this;
288 1
        $new->collapseOptions = $value;
289
290 1
        return $new;
291
    }
292
293
    /**
294
     * Set/remove Offcanvas::widget instead of collapse
295
     *
296
     * @param Offcanvas|null $offcanvas
297
     *
298
     * @return self
299
     */
300 2
    public function offcanvas(?Offcanvas $offcanvas): self
301
    {
302 2
        $new = clone $this;
303 2
        $new->offcanvas = $offcanvas;
304
305 2
        return $new;
306
    }
307
308
    /**
309
     * The text of the brand or empty if it's not used. Note that this is not HTML-encoded.
310
     *
311
     * @param string|null $value
312
     *
313
     * @return self
314
     *
315
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
316
     */
317 7
    public function brandText(?string $value): self
318
    {
319 7
        $new = clone $this;
320 7
        $new->brandText = $value;
321
322 7
        return $new;
323
    }
324
325
    /**
326
     * Src of the brand image or empty if it's not used. Note that this param will override `$this->brandText` param.
327
     *
328
     * @param string|null $value
329
     *
330
     * @return self
331
     *
332
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#image
333
     */
334 3
    public function brandImage(?string $value): self
335
    {
336 3
        $new = clone $this;
337 3
        $new->brandImage = $value;
338
339 3
        return $new;
340
    }
341
342
    /**
343
     * Set attributes for brandImage
344
     *
345
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
346
     *
347
     * @param array $attributes
348
     *
349
     * @return self
350
     */
351 1
    public function brandImageAttributes(array $attributes): self
352
    {
353 1
        $new = clone $this;
354 1
        $new->brandImageAttributes = $attributes;
355
356 1
        return $new;
357
    }
358
359
    /**
360
     * The URL for the brand's hyperlink tag and will be used for the "href" attribute of the brand link. Default value
361
     * is "/". You may set it to empty string if you want no link at all.
362
     *
363
     * @param string|null $value
364
     *
365
     * @return self
366
     *
367
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
368
     */
369 6
    public function brandUrl(?string $value): self
370
    {
371 6
        $new = clone $this;
372 6
        $new->brandUrl = $value;
373
374 6
        return $new;
375
    }
376
377
    /**
378
     * The HTML attributes of the brand link.
379
     *
380
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
381
     *
382
     * @param array $value
383
     *
384
     * @return self
385
     */
386 1
    public function brandOptions(array $value): self
387
    {
388 1
        $new = clone $this;
389 1
        $new->brandOptions = $value;
390
391 1
        return $new;
392
    }
393
394
    /**
395
     * Text to show for screen readers for the button to toggle the navbar.
396
     *
397
     * @param string $value
398
     *
399
     * @return self
400
     */
401 1
    public function screenReaderToggleText(string $value): self
402
    {
403 1
        $new = clone $this;
404 1
        $new->screenReaderToggleText = $value;
405
406 1
        return $new;
407
    }
408
409
    /**
410
     * The toggle button content. Defaults to bootstrap 4 default `<span class="navbar-toggler-icon"></span>`.
411
     *
412
     * @param string $value
413
     *
414
     * @return self
415
     */
416 1
    public function togglerContent(string $value): self
417
    {
418 1
        $new = clone $this;
419 1
        $new->togglerContent = $value;
420
421 1
        return $new;
422
    }
423
424
    /**
425
     * The HTML attributes of the navbar toggler button.
426
     *
427
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
428
     *
429
     * @param array $value
430
     *
431
     * @return self
432
     */
433 2
    public function togglerOptions(array $value): self
434
    {
435 2
        $new = clone $this;
436 2
        $new->togglerOptions = $value;
437
438 2
        return $new;
439
    }
440
441
    /**
442
     * This for a 100% width navbar.
443
     *
444
     * @return self
445
     */
446 1
    public function withoutRenderInnerContainer(): self
447
    {
448 1
        $new = clone $this;
449 1
        $new->renderInnerContainer = false;
450
451 1
        return $new;
452
    }
453
454
    /**
455
     * The HTML attributes of the inner container.
456
     *
457
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
458
     *
459
     * @param array $value
460
     *
461
     * @return self
462
     */
463 2
    public function innerContainerOptions(array $value): self
464
    {
465 2
        $new = clone $this;
466 2
        $new->innerContainerOptions = $value;
467
468 2
        return $new;
469
    }
470
471
    /**
472
     * The HTML attributes for the widget container tag. The following special options are recognized.
473
     *
474
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
475
     *
476
     * @param array $value
477
     *
478
     * @return self
479
     */
480 3
    public function options(array $value): self
481
    {
482 3
        $new = clone $this;
483 3
        $new->options = $value;
484
485 3
        return $new;
486
    }
487
488 20
    private function renderBrand(): string
489
    {
490 20
        if (empty($this->brandImage) && empty($this->brandText)) {
491 11
            return '';
492
        }
493
494 9
        $content = '';
495 9
        $options = $this->brandOptions;
496 9
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
497
498 9
        Html::addCssClass($options, ['widget' => 'navbar-brand']);
499
500 9
        if (!empty($this->brandImage)) {
501 3
            $encode = false;
502 3
            $content = Html::img($this->brandImage)->attributes($this->brandImageAttributes);
503
        }
504
505 9
        if (!empty($this->brandText)) {
506 7
            $content .= $this->brandText;
507
        }
508
        /** @var string|Stringable $content */
509 9
        if (empty($this->brandUrl)) {
510 1
            $brand = Html::span($content, $options);
511
        } else {
512 8
            $brand = Html::a($content, $this->brandUrl, $options);
513
        }
514
515
        return $brand
516 9
            ->encode($encode)
517 9
            ->render();
518
    }
519
520
    /**
521
     * Renders collapsible toggle button.
522
     *
523
     * @param string|null $targetId - ID of target element for current button
524
     *
525
     * @throws JsonException
526
     *
527
     * @return string the rendering toggle button.
528
     *
529
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#toggler
530
     */
531 19
    private function renderToggleButton(?string $targetId): string
532
    {
533 19
        $options = $this->togglerOptions;
534 19
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
535 19
        Html::addCssClass($options, ['widget' => 'navbar-toggler']);
536
537 19
        $defauts = [
538
            'type' => 'button',
539
            'data' => [
540 19
                'bs-toggle' => $this->offcanvas ? 'offcanvas' : 'collapse',
541
            ],
542
            'aria' => [
543 19
                'controls' => $targetId,
544
                'expanded' => 'false',
545 19
                'label' => $this->screenReaderToggleText,
546
            ],
547
        ];
548
549 19
        if ($targetId) {
550 18
            $defauts['data']['bs-target'] = '#' . $targetId;
551
        }
552
553 19
        return Html::button(
554 19
            $this->togglerContent,
555 19
            ArrayHelper::merge($defauts, $options)
556
        )
557 19
            ->encode($encode)
558 19
            ->render();
559
    }
560
}
561