Passed
Push — master ( 438f38...27fec6 )
by Wilmer
02:48
created

NavBar::begin()   F

Complexity

Conditions 13
Paths 384

Size

Total Lines 61
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 13

Importance

Changes 0
Metric Value
cc 13
eloc 36
nc 384
nop 0
dl 0
loc 61
ccs 37
cts 37
cp 1
crap 13
rs 3.4833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Bootstrap5;
6
7
use JsonException;
8
use Stringable;
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
    private array $collapseOptions = [];
124
    private ?string $brandText = null;
125
    private ?string $brandImage = null;
126
    private array $brandImageAttributes = [];
127
    private ?string $brandUrl = '/';
128
    private array $brandOptions = [];
129
    private string $screenReaderToggleText = 'Toggle navigation';
130
    private string $togglerContent = '<span class="navbar-toggler-icon"></span>';
131
    private array $togglerOptions = [];
132
    private bool $renderInnerContainer = true;
133
    private array $innerContainerOptions = [];
134
    private array $options = [];
135
    private bool $encodeTags = false;
136
    private ?string $expandSize = self::EXPAND_LG;
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 1
            $options['data-bs-theme'] = $this->theme;
162
163 1
            if ($this->theme === self::THEME_DARK) {
164 1
                $classNames['theme'] = 'navbar-dark';
165 1
            } elseif ($this->theme === self::THEME_LIGHT) {
166 1
                $classNames['theme'] = 'navbar-light';
167
            }
168
        }
169
170 20
        Html::addCssClass($options, $classNames);
171
172 20
        if (!isset($this->innerContainerOptions['class'])) {
173 18
            Html::addCssClass($this->innerContainerOptions, ['innerContainerOptions' => 'container']);
174
        }
175
176 20
        $htmlStart = Html::openTag($navTag, $options);
177
178 20
        if ($this->renderInnerContainer) {
179 19
            $htmlStart .= Html::openTag('div', $this->innerContainerOptions);
180
        }
181
182 20
        $htmlStart .= $this->renderBrand();
183
184 20
        if ($offcanvas) {
185 2
            $offcanvasId = $this->offcanvas ? $this->offcanvas->getId() : null;
186 2
            $htmlStart .= $this->renderToggleButton($offcanvasId);
187 2
            $htmlStart .= $offcanvas;
188 18
        } elseif ($this->expandSize) {
189 16
            $collapseOptions = $this->collapseOptions;
190 16
            $collapseTag = ArrayHelper::remove($collapseOptions, 'tag', 'div');
191
192 16
            if (!isset($collapseOptions['id'])) {
193 16
                $collapseOptions['id'] = $options['id'] . '-collapse';
194
            }
195
196 16
            Html::addCssClass($collapseOptions, ['collapse' => 'collapse', 'widget' => 'navbar-collapse']);
197
198 16
            $htmlStart .= $this->renderToggleButton($collapseOptions['id']);
199 16
            $htmlStart .= Html::openTag($collapseTag, $collapseOptions);
200 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...
201 1
            $htmlStart .= $this->renderToggleButton(null);
202
        }
203
204 20
        return $htmlStart;
205
    }
206
207 20
    public function render(): string
208
    {
209 20
        $htmlRun = '';
210
211 20
        if ($this->offcanvas) {
212 2
            $htmlRun = $this->offcanvas::end();
213 18
        } elseif ($this->expandSize) {
214 16
            $tag = ArrayHelper::getValue($this->collapseOptions, 'tag', 'div');
215 16
            $htmlRun = Html::closeTag($tag);
216
        }
217
218 20
        if ($this->renderInnerContainer) {
219 19
            $htmlRun .= Html::closeTag('div');
220
        }
221
222 20
        $tag = ArrayHelper::getValue($this->options, 'tag', 'nav');
223
224 20
        $htmlRun .= Html::closeTag($tag);
225
226 20
        return $htmlRun;
227
    }
228
229
    /**
230
     * Set size before then content will be expanded
231
     */
232 5
    public function expandSize(?string $size): self
233
    {
234 5
        $new = clone $this;
235 5
        $new->expandSize = $size;
236
237 5
        return $new;
238
    }
239
240
    /**
241
     * The HTML attributes for the container tag. The following special options are recognized.
242
     *
243
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
244
     */
245 1
    public function collapseOptions(array $value): self
246
    {
247 1
        $new = clone $this;
248 1
        $new->collapseOptions = $value;
249
250 1
        return $new;
251
    }
252
253
    /**
254
     * Set/remove Offcanvas::widget instead of collapse
255
     */
256 2
    public function offcanvas(?Offcanvas $offcanvas): self
257
    {
258 2
        $new = clone $this;
259 2
        $new->offcanvas = $offcanvas;
260
261 2
        return $new;
262
    }
263
264
    /**
265
     * The text of the brand or empty if it's not used. Note that this is not HTML-encoded.
266
     *
267
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
268
     */
269 7
    public function brandText(?string $value): self
270
    {
271 7
        $new = clone $this;
272 7
        $new->brandText = $value;
273
274 7
        return $new;
275
    }
276
277
    /**
278
     * Src of the brand image or empty if it's not used. Note that this param will override `$this->brandText` param.
279
     *
280
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#image
281
     */
282 3
    public function brandImage(?string $value): self
283
    {
284 3
        $new = clone $this;
285 3
        $new->brandImage = $value;
286
287 3
        return $new;
288
    }
289
290
    /**
291
     * Set attributes for brandImage
292
     *
293
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
294
     */
295 1
    public function brandImageAttributes(array $attributes): self
296
    {
297 1
        $new = clone $this;
298 1
        $new->brandImageAttributes = $attributes;
299
300 1
        return $new;
301
    }
302
303
    /**
304
     * The URL for the brand's hyperlink tag and will be used for the "href" attribute of the brand link. Default value
305
     * is "/". You may set it to empty string if you want no link at all.
306
     *
307
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#text
308
     */
309 6
    public function brandUrl(?string $value): self
310
    {
311 6
        $new = clone $this;
312 6
        $new->brandUrl = $value;
313
314 6
        return $new;
315
    }
316
317
    /**
318
     * The HTML attributes of the brand link.
319
     *
320
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
321
     */
322 1
    public function brandOptions(array $value): self
323
    {
324 1
        $new = clone $this;
325 1
        $new->brandOptions = $value;
326
327 1
        return $new;
328
    }
329
330
    /**
331
     * Text to show for screen readers for the button to toggle the navbar.
332
     */
333 1
    public function screenReaderToggleText(string $value): self
334
    {
335 1
        $new = clone $this;
336 1
        $new->screenReaderToggleText = $value;
337
338 1
        return $new;
339
    }
340
341
    /**
342
     * The toggle button content. Defaults to bootstrap 4 default `<span class="navbar-toggler-icon"></span>`.
343
     */
344 1
    public function togglerContent(string $value): self
345
    {
346 1
        $new = clone $this;
347 1
        $new->togglerContent = $value;
348
349 1
        return $new;
350
    }
351
352
    /**
353
     * The HTML attributes of the navbar toggler button.
354
     *
355
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
356
     */
357 2
    public function togglerOptions(array $value): self
358
    {
359 2
        $new = clone $this;
360 2
        $new->togglerOptions = $value;
361
362 2
        return $new;
363
    }
364
365
    /**
366
     * This for a 100% width navbar.
367
     */
368 1
    public function withoutRenderInnerContainer(): self
369
    {
370 1
        $new = clone $this;
371 1
        $new->renderInnerContainer = false;
372
373 1
        return $new;
374
    }
375
376
    /**
377
     * The HTML attributes of the inner container.
378
     *
379
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
380
     */
381 2
    public function innerContainerOptions(array $value): self
382
    {
383 2
        $new = clone $this;
384 2
        $new->innerContainerOptions = $value;
385
386 2
        return $new;
387
    }
388
389
    /**
390
     * The HTML attributes for the widget container tag. The following special options are recognized.
391
     *
392
     * {@see Html::renderTagAttributes()} for details on how attributes are being rendered.
393
     */
394 3
    public function options(array $value): self
395
    {
396 3
        $new = clone $this;
397 3
        $new->options = $value;
398
399 3
        return $new;
400
    }
401
402 20
    private function renderBrand(): string
403
    {
404 20
        if (empty($this->brandImage) && empty($this->brandText)) {
405 11
            return '';
406
        }
407
408 9
        $content = '';
409 9
        $options = $this->brandOptions;
410 9
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
411
412 9
        Html::addCssClass($options, ['widget' => 'navbar-brand']);
413
414 9
        if (!empty($this->brandImage)) {
415 3
            $encode = false;
416 3
            $content = Html::img($this->brandImage)->addAttributes($this->brandImageAttributes);
417
        }
418
419 9
        if (!empty($this->brandText)) {
420 7
            $content .= $this->brandText;
421
        }
422
        /** @var string|Stringable $content */
423 9
        if (empty($this->brandUrl)) {
424 1
            $brand = Html::span($content, $options);
425
        } else {
426 8
            $brand = Html::a($content, $this->brandUrl, $options);
427
        }
428
429 9
        return $brand
430 9
            ->encode($encode)
431 9
            ->render();
432
    }
433
434
    /**
435
     * Renders collapsible toggle button.
436
     *
437
     * @param string|null $targetId - ID of target element for current button
438
     *
439
     * @throws JsonException
440
     *
441
     * @return string the rendering toggle button.
442
     *
443
     * @link https://getbootstrap.com/docs/5.0/components/navbar/#toggler
444
     */
445 19
    private function renderToggleButton(?string $targetId): string
446
    {
447 19
        $options = $this->togglerOptions;
448 19
        $encode = ArrayHelper::remove($options, 'encode', $this->encodeTags);
449 19
        Html::addCssClass($options, ['widget' => 'navbar-toggler']);
450
451 19
        $defauts = [
452 19
            'type' => 'button',
453 19
            'data' => [
454 19
                'bs-toggle' => $this->offcanvas ? 'offcanvas' : 'collapse',
455 19
            ],
456 19
            'aria' => [
457 19
                'controls' => $targetId,
458 19
                'expanded' => 'false',
459 19
                'label' => $this->screenReaderToggleText,
460 19
            ],
461 19
        ];
462
463 19
        if ($targetId) {
464 18
            $defauts['data']['bs-target'] = '#' . $targetId;
465
        }
466
467 19
        return Html::button(
468 19
            $this->togglerContent,
469 19
            ArrayHelper::merge($defauts, $options)
470 19
        )
471 19
            ->encode($encode)
472 19
            ->render();
473
    }
474
}
475