Completed
Push — master ( 92c51b...1c2d7c )
by Wilmer
02:09 queued 02:09
created

NavBar   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Test Coverage

Coverage 97.14%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 147
dl 0
loc 440
ccs 136
cts 140
cp 0.9714
rs 9.1199
c 1
b 0
f 0
wmc 41

22 Methods

Rating   Name   Duplication   Size   Complexity  
B begin() 0 54 10
A run() 0 20 4
A expandSize() 0 6 1
A theme() 0 6 1
A getId() 0 3 1
A togglerContent() 0 6 1
A brandOptions() 0 6 1
A brandImage() 0 6 1
A togglerOptions() 0 6 1
A dark() 0 3 1
A options() 0 6 1
A brandText() 0 6 1
A brandImageAttributes() 0 6 1
A withoutRenderInnerContainer() 0 6 1
A collapseOptions() 0 6 1
A renderBrand() 0 28 6
A renderToggleButton() 0 26 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
 *    <?php 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->getUri()->getPath();
78
 *        $output = $this->render('index', ['currentPath' => $currentPath]);
79
 *        $response->getBody()->write($output);
80
 *
81
 *        return $response;
82
 *    }
83
 * ```
84
 *
85
 * Controller.php
86
 *
87
 * ```php
88
 *    private function renderContent($content, array $parameters = []): string
89
 *    {
90
 *        $user = $this->user->getIdentity();
91
 *        $layout = $this->findLayoutFile($this->layout);
92
 *
93
 *        if ($layout !== null) {
94
 *            return $this->view->renderFile(
95
 *                $layout,
96
 *                    [
97
 *                        'aliases' => $this->aliases,
98
 *                        'content' => $content,
99
 *                        'user' => $user,
100
 *                        'params' => $this->params,
101
 *                        'currentPath' => !isset($parameters['currentPath']) ?: $parameters['currentPath']
102
 *                    ],
103
 *                $this
104
 *            );
105
 *        }
106
 *
107
 *        return $content;
108
 *    }
109
 * ```
110
 */
111
final class NavBar extends Widget
112
{
113
    public const EXPAND_SM = 'navbar-expand-sm';
114
    public const EXPAND_MD = 'navbar-expand-md';
115
    public const EXPAND_LG = 'navbar-expand-lg';
116
    public const EXPAND_XL = 'navbar-expand-xl';
117
    public const EXPAND_XXL = 'navbar-expand-xxl';
118
119
    public const THEME_LIGHT = 'navbar-light';
120
    public const THEME_DARK = 'navbar-dark';
121
122
    private array $collapseOptions = [];
123
    private ?string $brandText = null;
124
    private ?string $brandImage = null;
125
    private array $brandImageAttributes = [];
126
    private ?string $brandUrl = '/';
127
    private array $brandOptions = [];
128
    private string $screenReaderToggleText = 'Toggle navigation';
129
    private string $togglerContent = '<span class="navbar-toggler-icon"></span>';
130
    private array $togglerOptions = [];
131
    private bool $renderInnerContainer = true;
132
    private array $innerContainerOptions = [];
133
    private array $options = [];
134
    private bool $encodeTags = false;
135
    private ?string $expandSize = self::EXPAND_LG;
136
    private ?string $theme = self::THEME_LIGHT;
137
    private ?Offcanvas $offcanvas = null;
138
139 20
    public function getId(?string $suffix = '-navbar'): ?string
140
    {
141 20
        return $this->options['id'] ?? parent::getId($suffix);
142
    }
143
144 20
    public function begin(): string
145
    {
146
        /** Run Offcanvas::begin before NavBar parent::begin for right stack order */
147 20
        $offcanvas = $this->offcanvas ? $this->offcanvas->begin() : null;
148
149 20
        parent::begin();
150
151 20
        $options = $this->options;
152 20
        $options['id'] = $this->getId();
153 20
        $navTag = ArrayHelper::remove($options, 'tag', 'nav');
154 20
        $classNames = ['widget' => 'navbar'];
155
156 20
        if ($this->expandSize) {
157 17
            $classNames['size'] = $this->expandSize;
158
        }
159
160 20
        if ($this->theme) {
161 19
            $classNames['theme'] = $this->theme;
162
        }
163
164 20
        Html::addCssClass($options, $classNames);
165
166 20
        if (!isset($this->innerContainerOptions['class'])) {
167 18
            Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']);
168
        }
169
170 20
        $htmlStart = Html::openTag($navTag, $options);
171
172 20
        if ($this->renderInnerContainer) {
173 19
            $htmlStart .= Html::openTag('div', $this->innerContainerOptions);
174
        }
175
176 20
        $htmlStart .= $this->renderBrand();
177
178 20
        if ($offcanvas) {
179 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

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