GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( e52ef6...deed81 )
by Sebastian
15:30
created

Menu::renderItem()   C

Complexity

Conditions 12
Paths 82

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 6.9666
c 0
b 0
f 0
cc 12
nc 82
nop 1

How to fix   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
namespace Spatie\Menu;
4
5
use ArrayIterator;
6
use Countable;
7
use IteratorAggregate;
8
use Spatie\Menu\Html\Tag;
9
use Spatie\Menu\Html\Attributes;
10
use Spatie\Menu\Helpers\Reflection;
11
use Spatie\Menu\Traits\Conditions as ConditionsTrait;
12
use Spatie\Menu\Traits\HasTextAttributes as HasAttributesTrait;
13
use Spatie\Menu\Traits\HasHtmlAttributes as HasHtmlAttributesTrait;
14
use Spatie\Menu\Traits\HasParentAttributes as HasParentAttributesTrait;
15
use Traversable;
16
17
class Menu implements Item, Countable, HasHtmlAttributes, HasParentAttributes, IteratorAggregate
18
{
19
    use HasHtmlAttributesTrait, HasParentAttributesTrait, ConditionsTrait, HasAttributesTrait;
20
21
    /** @var array */
22
    protected $items = [];
23
24
    /** @var array */
25
    protected $filters = [];
26
27
    /** @var string */
28
    protected $prepend, $append = '';
29
30
    /** @var array */
31
    protected $wrap = [];
32
33
    /** @var string */
34
    protected $activeClass = 'active';
35
36
    /** @var string */
37
    protected $exactActiveClass = 'exact-active';
38
39
    /** @var string */
40
    protected $wrapperTagName = 'ul';
41
42
    /** @var bool */
43
    protected $parentTagName = 'li';
44
45
    /** @var bool */
46
    protected $activeClassOnParent = true;
47
48
    /** @var bool */
49
    protected $activeClassOnLink = false;
50
51
    /** @var \Spatie\Menu\Html\Attributes */
52
    protected $htmlAttributes, $parentAttributes;
53
54
    protected function __construct(Item ...$items)
55
    {
56
        $this->items = $items;
57
58
        $this->htmlAttributes = new Attributes();
59
        $this->parentAttributes = new Attributes();
60
    }
61
62
    /**
63
     * Create a new menu, optionally prefilled with items.
64
     *
65
     * @param array $items
66
     *
67
     * @return static
68
     */
69
    public static function new($items = [])
70
    {
71
        return new static(...array_values($items));
72
    }
73
74
    /**
75
     * Build a new menu from an array. The callback receives a menu instance as
76
     * the accumulator, the array item as the second parameter, and the item's
77
     * key as the third.
78
     *
79
     * @param array|\Iterator $items
80
     * @param callable $callback
81
     * @param \Spatie\Menu\Menu|null $initial
82
     *
83
     * @return static
84
     */
85
    public static function build($items, callable $callback, self $initial = null)
86
    {
87
        return ($initial ?: static::new())->fill($items, $callback);
88
    }
89
90
    /**
91
     * Fill a menu from an array. The callback receives a menu instance as
92
     * the accumulator, the array item as the second parameter, and the item's
93
     * key as the third.
94
     *
95
     * @param array|\Iterator $items
96
     * @param callable $callback
97
     *
98
     * @return static
99
     */
100
    public function fill($items, callable $callback)
101
    {
102
        $menu = $this;
103
104
        foreach ($items as $key => $item) {
105
            $menu = $callback($menu, $item, $key) ?: $menu;
106
        }
107
108
        return $menu;
109
    }
110
111
    /**
112
     * Add an item to the menu. This also applies all registered filters to the
113
     * item.
114
     *
115
     * @param \Spatie\Menu\Item $item
116
     *
117
     * @return $this
118
     */
119
    public function add(Item $item)
120
    {
121
        foreach ($this->filters as $filter) {
122
            $this->applyFilter($filter, $item);
123
        }
124
125
        $this->items[] = $item;
126
127
        return $this;
128
    }
129
130
    /**
131
     * Add an item to the menu if a (non-strict) condition is met.
132
     *
133
     * @param bool $condition
134
     * @param \Spatie\Menu\Item $item
135
     *
136
     * @return $this
137
     */
138
    public function addIf($condition, Item $item)
139
    {
140
        if ($this->resolveCondition($condition)) {
141
            $this->add($item);
142
        }
143
144
        return $this;
145
    }
146
147
    /**
148
     * Shortcut function to add a plain link to the menu.
149
     *
150
     * @param string $url
151
     * @param string $text
152
     *
153
     * @return $this
154
     */
155
    public function link(string $url, string $text)
156
    {
157
        return $this->add(Link::to($url, $text));
158
    }
159
160
    /**
161
     * Shortcut function to add an empty item to the menu.
162
     *
163
     * @return $this
164
     */
165
    public function empty()
166
    {
167
        return $this->add(Html::empty());
168
    }
169
170
    /**
171
     * Add a link to the menu if a (non-strict) condition is met.
172
     *
173
     * @param bool $condition
174
     * @param string $url
175
     * @param string $text
176
     *
177
     * @return $this
178
     */
179
    public function linkIf($condition, string $url, string $text)
180
    {
181
        if ($this->resolveCondition($condition)) {
182
            $this->link($url, $text);
183
        }
184
185
        return $this;
186
    }
187
188
    /**
189
     * Shortcut function to add raw html to the menu.
190
     *
191
     * @param string $html
192
     * @param array $parentAttributes
193
     *
194
     * @return $this
195
     */
196
    public function html(string $html, array $parentAttributes = [])
197
    {
198
        return $this->add(Html::raw($html)->setParentAttributes($parentAttributes));
199
    }
200
201
    /**
202
     * Add a chunk of html if a (non-strict) condition is met.
203
     *
204
     * @param bool $condition
205
     * @param string $html
206
     * @param array $parentAttributes
207
     *
208
     * @return $this
209
     */
210
    public function htmlIf($condition, string $html, array $parentAttributes = [])
211
    {
212
        if ($this->resolveCondition($condition)) {
213
            $this->html($html, $parentAttributes);
214
        }
215
216
        return $this;
217
    }
218
219
    /**
220
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
221
     * @param callable|\Spatie\Menu\Menu|null $menu
222
     *
223
     * @return $this
224
     */
225
    public function submenu($header, $menu = null)
0 ignored issues
show
Unused Code introduced by
The parameter $header is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $menu is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
226
    {
227
        list($header, $menu) = $this->parseSubmenuArgs(func_get_args());
228
229
        $menu = $this->createSubmenuMenu($menu);
230
        $header = $this->createSubmenuHeader($header);
231
232
        return $this->add($menu->prependIf($header, $header));
0 ignored issues
show
Documentation introduced by
$header is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
233
    }
234
235
    /**
236
     * @param bool $condition
237
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
238
     * @param callable|\Spatie\Menu\Menu|null $menu
239
     *
240
     * @return $this
241
     */
242
    public function submenuIf($condition, $header, $menu = null)
243
    {
244
        if ($condition) {
245
            $this->submenu($header, $menu);
246
        }
247
248
        return $this;
249
    }
250
251
    protected function parseSubmenuArgs($args): array
252
    {
253
        if (count($args) === 1) {
254
            return ['', $args[0]];
255
        }
256
257
        return [$args[0], $args[1]];
258
    }
259
260
    /**
261
     * @param \Spatie\Menu\Menu|callable $menu
262
     *
263
     * @return \Spatie\Menu\Menu
264
     */
265
    protected function createSubmenuMenu($menu): self
266
    {
267
        if (is_callable($menu)) {
268
            $transformer = $menu;
269
            $menu = $this->blueprint();
270
            $transformer($menu);
271
        }
272
273
        return $menu;
274
    }
275
276
    /**
277
     * @param \Spatie\Menu\Item|string $header
278
     *
279
     * @return string
280
     */
281
    protected function createSubmenuHeader($header): string
282
    {
283
        if ($header instanceof Item) {
284
            $header = $header->render();
285
        }
286
287
        return $header;
288
    }
289
290
    /**
291
     * Iterate over all the items and apply a callback. If you typehint the
292
     * item parameter in the callable, it wil only be applied to items of that
293
     * type.
294
     *
295
     * @param callable $callable
296
     *
297
     * @return $this
298
     */
299
    public function each(callable $callable)
300
    {
301
        $type = Reflection::firstParameterType($callable);
302
303
        foreach ($this->items as $item) {
304
            if (! Reflection::itemMatchesType($item, $type)) {
305
                continue;
306
            }
307
308
            $callable($item);
309
        }
310
311
        return $this;
312
    }
313
314
    /**
315
     * Register a filter to the menu. When an item is added, all filters will be
316
     * applied to the item. If you typehint the item parameter in the callable, it
317
     * will only be applied to items of that type.
318
     *
319
     * @param callable $callable
320
     *
321
     * @return $this
322
     */
323
    public function registerFilter(callable $callable)
324
    {
325
        $this->filters[] = $callable;
326
327
        return $this;
328
    }
329
330
    /**
331
     * Apply a filter to an item. Returns the result of the filter.
332
     *
333
     * @param callable $filter
334
     * @param \Spatie\Menu\Item $item
335
     */
336
    protected function applyFilter(callable $filter, Item $item)
337
    {
338
        $type = Reflection::firstParameterType($filter);
339
340
        if (! Reflection::itemMatchesType($item, $type)) {
341
            return;
342
        }
343
344
        $filter($item);
345
    }
346
347
    /**
348
     * Apply a callable to all existing items, and register it as a filter so it
349
     * will get applied to all new items too. If you typehint the item parameter
350
     * in the callable, it wil only be applied to items of that type.
351
     *
352
     * @param callable $callable
353
     *
354
     * @return $this
355
     */
356
    public function applyToAll(callable $callable)
357
    {
358
        $this->each($callable);
359
        $this->registerFilter($callable);
360
361
        return $this;
362
    }
363
364
    /**
365
     * Wrap the entire menu in an html element. This is another level of
366
     * wrapping above the `wrapperTag`.
367
     *
368
     * @param string $element
369
     * @param array $attributes
370
     *
371
     * @return $this
372
     */
373
    public function wrap(string $element, $attributes = [])
374
    {
375
        $this->wrap = [$element, $attributes];
376
377
        return $this;
378
    }
379
380
    /**
381
     * Determine whether the menu is active.
382
     *
383
     * @return bool
384
     */
385
    public function isActive(): bool
386
    {
387
        foreach ($this->items as $item) {
388
            if ($item->isActive()) {
389
                return true;
390
            }
391
        }
392
393
        return false;
394
    }
395
396
    /**
397
     * A menu can be active but not exact-active.
398
     *
399
     * @return bool
400
     */
401
    public function isExactActive(): bool
402
    {
403
        return false;
404
    }
405
406
    /**
407
     * Set multiple items in the menu as active based on a callable that filters
408
     * through items. If you typehint the item parameter in the callable, it will
409
     * only be applied to items of that type.
410
     *
411
     * @param callable|string $urlOrCallable
412
     * @param string $root
413
     *
414
     * @return $this
415
     */
416
    public function setActive($urlOrCallable, string $root = '/')
417
    {
418
        if (is_string($urlOrCallable)) {
419
            return $this->setActiveFromUrl($urlOrCallable, $root);
420
        }
421
422
        if (is_callable($urlOrCallable)) {
423
            return $this->setActiveFromCallable($urlOrCallable);
424
        }
425
426
        throw new \InvalidArgumentException('`setActive` requires a pattern or a callable');
427
    }
428
429
    /**
430
     * Set the class name that will be used on exact-active items for this menu.
431
     *
432
     * @param string $class
433
     *
434
     * @return $this
435
     */
436
    public function setExactActiveClass(string $class)
437
    {
438
        $this->exactActiveClass = $class;
439
440
        return $this;
441
    }
442
443
    /**
444
     * Set all relevant children active based on the current request's URL.
445
     *
446
     * /, /about, /contact => request to /about will set the about link active.
447
     *
448
     * /en, /en/about, /en/contact => request to /en won't set /en active if the
449
     *                                request root is set to /en.
450
     *
451
     * @param string $url  The current request url.
452
     * @param string $root If the link's URL is an exact match with the request
453
     *                     root, the link won't be set active. This behavior is
454
     *                     to avoid having home links active on every request.
455
     *
456
     * @return $this
457
     */
458
    public function setActiveFromUrl(string $url, string $root = '/')
459
    {
460
        $this->applyToAll(function (Menu $menu) use ($url, $root) {
461
            $menu->setActiveFromUrl($url, $root);
462
        });
463
464
        $this->applyToAll(function (Activatable $item) use ($url, $root) {
465
            $item->determineActiveForUrl($url, $root);
466
        });
467
468
        return $this;
469
    }
470
471
    /**
472
     * @param callable $callable
473
     *
474
     * @return $this
475
     */
476
    public function setActiveFromCallable(callable $callable)
477
    {
478
        $this->applyToAll(function (Menu $menu) use ($callable) {
479
            $menu->setActiveFromCallable($callable);
480
        });
481
482
        $type = Reflection::firstParameterType($callable);
483
484
        $this->applyToAll(function (Activatable $item) use ($callable, $type) {
485
            if (! Reflection::itemMatchesType($item, $type)) {
0 ignored issues
show
Documentation introduced by
$item is of type object<Spatie\Menu\Activatable>, but the function expects a object<Spatie\Menu\Item>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
486
                return;
487
            }
488
489
            if ($callable($item)) {
490
                $item->setActive();
491
                $item->setExactActive();
492
            }
493
        });
494
495
        return $this;
496
    }
497
498
    /**
499
     * Set the class name that will be used on active items for this menu.
500
     *
501
     * @param string $class
502
     *
503
     * @return $this
504
     */
505
    public function setActiveClass(string $class)
506
    {
507
        $this->activeClass = $class;
508
509
        return $this;
510
    }
511
512
    /**
513
     * Add a class to all items in the menu.
514
     *
515
     * @param string $class
516
     *
517
     * @return $this
518
     */
519
    public function addItemClass(string $class)
520
    {
521
        $this->applyToAll(function (HasHtmlAttributes $link) use ($class) {
522
            $link->addClass($class);
523
        });
524
525
        return $this;
526
    }
527
528
    /**
529
     * Set an attribute on all items in the menu.
530
     *
531
     * @param string $attribute
532
     * @param string $value
533
     *
534
     * @return $this
535
     */
536
    public function setItemAttribute(string $attribute, string $value = '')
537
    {
538
        $this->applyToAll(function (HasHtmlAttributes $link) use ($attribute, $value) {
539
            $link->setAttribute($attribute, $value);
540
        });
541
542
        return $this;
543
    }
544
545
    /**
546
     * Add a parent class to all items in the menu.
547
     *
548
     * @param string $class
549
     *
550
     * @return $this
551
     */
552
    public function addItemParentClass(string $class)
553
    {
554
        $this->applyToAll(function (HasParentAttributes $item) use ($class) {
555
            $item->addParentClass($class);
556
        });
557
558
        return $this;
559
    }
560
561
    /**
562
     * Add a parent attribute to all items in the menu.
563
     *
564
     * @param string $attribute
565
     * @param string $value
566
     *
567
     * @return $this
568
     */
569
    public function setItemParentAttribute(string $attribute, string $value = '')
570
    {
571
        $this->applyToAll(function (HasParentAttributes $item) use ($attribute, $value) {
572
            $item->setParentAttribute($attribute, $value);
573
        });
574
575
        return $this;
576
    }
577
578
    /**
579
     * Set tag for items wrapper.
580
     *
581
     * @param string|null $wrapperTagName
582
     * @return $this
583
     */
584
    public function setWrapperTag($wrapperTagName = null)
585
    {
586
        $this->wrapperTagName = $wrapperTagName;
587
588
        return $this;
589
    }
590
591
    /**
592
     * Set tag for items wrapper.
593
     *
594
     * @param string|null $wrapperTagName
0 ignored issues
show
Bug introduced by
There is no parameter named $wrapperTagName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
595
     * @return $this
596
     */
597
    public function withoutWrapperTag()
598
    {
599
        $this->wrapperTagName = null;
600
601
        return $this;
602
    }
603
604
    /**
605
     * Set the parent tag name.
606
     *
607
     * @param string|null $parentTagName
608
     * @return $this
609
     */
610
    public function setParentTag($parentTagName = null)
611
    {
612
        $this->parentTagName = $parentTagName;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parentTagName can also be of type string. However, the property $parentTagName is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
613
614
        return $this;
615
    }
616
617
    /**
618
     * Render items without a parent tag.
619
     *
620
     * @return $this
621
     */
622
    public function withoutParentTag()
623
    {
624
        $this->parentTagName = null;
625
626
        return $this;
627
    }
628
629
    /**
630
     * Set whether active class should (also) be on link.
631
     *
632
     * @param $activeClassOnLink
633
     * @return $this
634
     */
635
    public function setActiveClassOnLink(bool $activeClassOnLink = true)
636
    {
637
        $this->activeClassOnLink = $activeClassOnLink;
638
639
        return $this;
640
    }
641
642
    /**
643
     * Set whether active class should (also) be on parent.
644
     *
645
     * @param $activeClassOnParent
646
     * @return $this
647
     */
648
    public function setActiveClassOnParent(bool $activeClassOnParent = true)
649
    {
650
        $this->activeClassOnParent = $activeClassOnParent;
651
652
        return $this;
653
    }
654
655
    /**
656
     * @param bool $condition
657
     * @param callable $callable
658
     *
659
     * @return $this
660
     */
661
    public function if(bool $condition, callable $callable)
662
    {
663
        return $condition ? $callable($this) : $this;
664
    }
665
666
    /**
667
     * Create a empty blueprint of the menu (copies `filters` and `activeClass`).
668
     *
669
     * @return static
670
     */
671
    public function blueprint()
672
    {
673
        $clone = new static();
674
675
        $clone->filters = $this->filters;
676
        $clone->activeClass = $this->activeClass;
677
678
        return $clone;
679
    }
680
681
    /**
682
     * Render the menu.
683
     *
684
     * @return string
685
     */
686
    public function render(): string
687
    {
688
        $tag = $this->wrapperTagName
689
            ? new Tag($this->wrapperTagName, $this->htmlAttributes)
690
            : null;
691
692
        $contents = array_map([$this, 'renderItem'], $this->items);
693
694
        $wrappedContents = $tag ? $tag->withContents($contents) : implode('', $contents);
695
696
        $menu = $this->prepend.$wrappedContents.$this->append;
697
698
        if (! empty($this->wrap)) {
699
            return Tag::make($this->wrap[0], new Attributes($this->wrap[1]))->withContents($menu);
700
        }
701
702
        return $menu;
703
    }
704
705
    protected function renderItem(Item $item): string
706
    {
707
        $attributes = new Attributes();
708
709
        if (method_exists($item, 'beforeRender')) {
710
            $item->beforeRender();
0 ignored issues
show
Bug introduced by
The method beforeRender() does not exist on Spatie\Menu\Item. Did you maybe mean render()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
711
        }
712
713
        if (method_exists($item, 'willRender') && $item->willRender() === false) {
0 ignored issues
show
Bug introduced by
The method willRender() does not exist on Spatie\Menu\Item. Did you maybe mean render()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
714
            return '';
715
        }
716
717
        if ($item->isActive()) {
718
            if ($this->activeClassOnParent) {
719
                $attributes->addClass($this->activeClass);
720
721
                if ($item->isExactActive()) {
722
                    $attributes->addClass($this->exactActiveClass);
723
                }
724
            }
725
726
            if ($this->activeClassOnLink && $item instanceof HasHtmlAttributes) {
727
                $item->addClass($this->activeClass);
728
729
                if ($item->isExactActive()) {
730
                    $item->addClass($this->exactActiveClass);
731
                }
732
            }
733
        }
734
735
        if ($item instanceof HasParentAttributes) {
736
            $attributes->setAttributes($item->parentAttributes());
737
        }
738
739
        if (! $this->parentTagName) {
740
            return $item->render();
741
        }
742
743
        return Tag::make($this->parentTagName, $attributes)->withContents($item->render());
0 ignored issues
show
Documentation introduced by
$this->parentTagName is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
744
    }
745
746
    /**
747
     * The amount of items in the menu.
748
     *
749
     * @return int
750
     */
751
    public function count(): int
752
    {
753
        return count($this->items);
754
    }
755
756
    /**
757
     * @return string
758
     */
759
    public function __toString(): string
760
    {
761
        return $this->render();
762
    }
763
764
    public function getIterator(): Traversable
765
    {
766
        return new ArrayIterator($this->items);
767
    }
768
}
769