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 ( f46544...e52ef6 )
by Sebastian
16:02 queued 13:23
created

Menu::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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 $wrapperTagName = 'ul';
38
39
    /** @var bool */
40
    protected $parentTagName = 'li';
41
42
    /** @var bool */
43
    protected $activeClassOnParent = true;
44
45
    /** @var bool */
46
    protected $activeClassOnLink = false;
47
48
    /** @var \Spatie\Menu\Html\Attributes */
49
    protected $htmlAttributes, $parentAttributes;
50
51
    protected function __construct(Item ...$items)
52
    {
53
        $this->items = $items;
54
55
        $this->htmlAttributes = new Attributes();
56
        $this->parentAttributes = new Attributes();
57
    }
58
59
    /**
60
     * Create a new menu, optionally prefilled with items.
61
     *
62
     * @param array $items
63
     *
64
     * @return static
65
     */
66
    public static function new($items = [])
67
    {
68
        return new static(...array_values($items));
69
    }
70
71
    /**
72
     * Build a new menu from an array. The callback receives a menu instance as
73
     * the accumulator, the array item as the second parameter, and the item's
74
     * key as the third.
75
     *
76
     * @param array|\Iterator $items
77
     * @param callable $callback
78
     * @param \Spatie\Menu\Menu|null $initial
79
     *
80
     * @return static
81
     */
82
    public static function build($items, callable $callback, self $initial = null)
83
    {
84
        return ($initial ?: static::new())->fill($items, $callback);
85
    }
86
87
    /**
88
     * Fill a menu from an array. The callback receives a menu instance as
89
     * the accumulator, the array item as the second parameter, and the item's
90
     * key as the third.
91
     *
92
     * @param array|\Iterator $items
93
     * @param callable $callback
94
     *
95
     * @return static
96
     */
97
    public function fill($items, callable $callback)
98
    {
99
        $menu = $this;
100
101
        foreach ($items as $key => $item) {
102
            $menu = $callback($menu, $item, $key) ?: $menu;
103
        }
104
105
        return $menu;
106
    }
107
108
    /**
109
     * Add an item to the menu. This also applies all registered filters to the
110
     * item.
111
     *
112
     * @param \Spatie\Menu\Item $item
113
     *
114
     * @return $this
115
     */
116
    public function add(Item $item)
117
    {
118
        foreach ($this->filters as $filter) {
119
            $this->applyFilter($filter, $item);
120
        }
121
122
        $this->items[] = $item;
123
124
        return $this;
125
    }
126
127
    /**
128
     * Add an item to the menu if a (non-strict) condition is met.
129
     *
130
     * @param bool $condition
131
     * @param \Spatie\Menu\Item $item
132
     *
133
     * @return $this
134
     */
135
    public function addIf($condition, Item $item)
136
    {
137
        if ($this->resolveCondition($condition)) {
138
            $this->add($item);
139
        }
140
141
        return $this;
142
    }
143
144
    /**
145
     * Shortcut function to add a plain link to the menu.
146
     *
147
     * @param string $url
148
     * @param string $text
149
     *
150
     * @return $this
151
     */
152
    public function link(string $url, string $text)
153
    {
154
        return $this->add(Link::to($url, $text));
155
    }
156
157
    /**
158
     * Shortcut function to add an empty item to the menu.
159
     *
160
     * @return $this
161
     */
162
    public function empty()
163
    {
164
        return $this->add(Html::empty());
165
    }
166
167
    /**
168
     * Add a link to the menu if a (non-strict) condition is met.
169
     *
170
     * @param bool $condition
171
     * @param string $url
172
     * @param string $text
173
     *
174
     * @return $this
175
     */
176
    public function linkIf($condition, string $url, string $text)
177
    {
178
        if ($this->resolveCondition($condition)) {
179
            $this->link($url, $text);
180
        }
181
182
        return $this;
183
    }
184
185
    /**
186
     * Shortcut function to add raw html to the menu.
187
     *
188
     * @param string $html
189
     * @param array $parentAttributes
190
     *
191
     * @return $this
192
     */
193
    public function html(string $html, array $parentAttributes = [])
194
    {
195
        return $this->add(Html::raw($html)->setParentAttributes($parentAttributes));
196
    }
197
198
    /**
199
     * Add a chunk of html if a (non-strict) condition is met.
200
     *
201
     * @param bool $condition
202
     * @param string $html
203
     * @param array $parentAttributes
204
     *
205
     * @return $this
206
     */
207
    public function htmlIf($condition, string $html, array $parentAttributes = [])
208
    {
209
        if ($this->resolveCondition($condition)) {
210
            $this->html($html, $parentAttributes);
211
        }
212
213
        return $this;
214
    }
215
216
    /**
217
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
218
     * @param callable|\Spatie\Menu\Menu|null $menu
219
     *
220
     * @return $this
221
     */
222
    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...
223
    {
224
        list($header, $menu) = $this->parseSubmenuArgs(func_get_args());
225
226
        $menu = $this->createSubmenuMenu($menu);
227
        $header = $this->createSubmenuHeader($header);
228
229
        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...
230
    }
231
232
    /**
233
     * @param bool $condition
234
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
235
     * @param callable|\Spatie\Menu\Menu|null $menu
236
     *
237
     * @return $this
238
     */
239
    public function submenuIf($condition, $header, $menu = null)
240
    {
241
        if ($condition) {
242
            $this->submenu($header, $menu);
243
        }
244
245
        return $this;
246
    }
247
248
    protected function parseSubmenuArgs($args): array
249
    {
250
        if (count($args) === 1) {
251
            return ['', $args[0]];
252
        }
253
254
        return [$args[0], $args[1]];
255
    }
256
257
    /**
258
     * @param \Spatie\Menu\Menu|callable $menu
259
     *
260
     * @return \Spatie\Menu\Menu
261
     */
262
    protected function createSubmenuMenu($menu): self
263
    {
264
        if (is_callable($menu)) {
265
            $transformer = $menu;
266
            $menu = $this->blueprint();
267
            $transformer($menu);
268
        }
269
270
        return $menu;
271
    }
272
273
    /**
274
     * @param \Spatie\Menu\Item|string $header
275
     *
276
     * @return string
277
     */
278
    protected function createSubmenuHeader($header): string
279
    {
280
        if ($header instanceof Item) {
281
            $header = $header->render();
282
        }
283
284
        return $header;
285
    }
286
287
    /**
288
     * Iterate over all the items and apply a callback. If you typehint the
289
     * item parameter in the callable, it wil only be applied to items of that
290
     * type.
291
     *
292
     * @param callable $callable
293
     *
294
     * @return $this
295
     */
296
    public function each(callable $callable)
297
    {
298
        $type = Reflection::firstParameterType($callable);
299
300
        foreach ($this->items as $item) {
301
            if (! Reflection::itemMatchesType($item, $type)) {
302
                continue;
303
            }
304
305
            $callable($item);
306
        }
307
308
        return $this;
309
    }
310
311
    /**
312
     * Register a filter to the menu. When an item is added, all filters will be
313
     * applied to the item. If you typehint the item parameter in the callable, it
314
     * will only be applied to items of that type.
315
     *
316
     * @param callable $callable
317
     *
318
     * @return $this
319
     */
320
    public function registerFilter(callable $callable)
321
    {
322
        $this->filters[] = $callable;
323
324
        return $this;
325
    }
326
327
    /**
328
     * Apply a filter to an item. Returns the result of the filter.
329
     *
330
     * @param callable $filter
331
     * @param \Spatie\Menu\Item $item
332
     */
333
    protected function applyFilter(callable $filter, Item $item)
334
    {
335
        $type = Reflection::firstParameterType($filter);
336
337
        if (! Reflection::itemMatchesType($item, $type)) {
338
            return;
339
        }
340
341
        $filter($item);
342
    }
343
344
    /**
345
     * Apply a callable to all existing items, and register it as a filter so it
346
     * will get applied to all new items too. If you typehint the item parameter
347
     * in the callable, it wil only be applied to items of that type.
348
     *
349
     * @param callable $callable
350
     *
351
     * @return $this
352
     */
353
    public function applyToAll(callable $callable)
354
    {
355
        $this->each($callable);
356
        $this->registerFilter($callable);
357
358
        return $this;
359
    }
360
361
    /**
362
     * Wrap the entire menu in an html element. This is another level of
363
     * wrapping above the `wrapperTag`.
364
     *
365
     * @param string $element
366
     * @param array $attributes
367
     *
368
     * @return $this
369
     */
370
    public function wrap(string $element, $attributes = [])
371
    {
372
        $this->wrap = [$element, $attributes];
373
374
        return $this;
375
    }
376
377
    /**
378
     * Determine whether the menu is active.
379
     *
380
     * @return bool
381
     */
382
    public function isActive(): bool
383
    {
384
        foreach ($this->items as $item) {
385
            if ($item->isActive()) {
386
                return true;
387
            }
388
        }
389
390
        return false;
391
    }
392
393
    /**
394
     * Set multiple items in the menu as active based on a callable that filters
395
     * through items. If you typehint the item parameter in the callable, it will
396
     * only be applied to items of that type.
397
     *
398
     * @param callable|string $urlOrCallable
399
     * @param string $root
400
     *
401
     * @return $this
402
     */
403
    public function setActive($urlOrCallable, string $root = '/')
404
    {
405
        if (is_string($urlOrCallable)) {
406
            return $this->setActiveFromUrl($urlOrCallable, $root);
407
        }
408
409
        if (is_callable($urlOrCallable)) {
410
            return $this->setActiveFromCallable($urlOrCallable);
411
        }
412
413
        throw new \InvalidArgumentException('`setActive` requires a pattern or a callable');
414
    }
415
416
    /**
417
     * Set all relevant children active based on the current request's URL.
418
     *
419
     * /, /about, /contact => request to /about will set the about link active.
420
     *
421
     * /en, /en/about, /en/contact => request to /en won't set /en active if the
422
     *                                request root is set to /en.
423
     *
424
     * @param string $url  The current request url.
425
     * @param string $root If the link's URL is an exact match with the request
426
     *                     root, the link won't be set active. This behavior is
427
     *                     to avoid having home links active on every request.
428
     *
429
     * @return $this
430
     */
431
    public function setActiveFromUrl(string $url, string $root = '/')
432
    {
433
        $this->applyToAll(function (Menu $menu) use ($url, $root) {
434
            $menu->setActiveFromUrl($url, $root);
435
        });
436
437
        $this->applyToAll(function (Activatable $item) use ($url, $root) {
438
            $item->determineActiveForUrl($url, $root);
439
        });
440
441
        return $this;
442
    }
443
444
    /**
445
     * @param callable $callable
446
     *
447
     * @return $this
448
     */
449
    public function setActiveFromCallable(callable $callable)
450
    {
451
        $this->applyToAll(function (Menu $menu) use ($callable) {
452
            $menu->setActiveFromCallable($callable);
453
        });
454
455
        $type = Reflection::firstParameterType($callable);
456
457
        $this->applyToAll(function (Activatable $item) use ($callable, $type) {
458
            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...
459
                return;
460
            }
461
462
            if ($callable($item)) {
463
                $item->setActive();
464
            }
465
        });
466
467
        return $this;
468
    }
469
470
    /**
471
     * Set the class name that will be used on active items for this menu.
472
     *
473
     * @param string $class
474
     *
475
     * @return $this
476
     */
477
    public function setActiveClass(string $class)
478
    {
479
        $this->activeClass = $class;
480
481
        return $this;
482
    }
483
484
    /**
485
     * Add a class to all items in the menu.
486
     *
487
     * @param string $class
488
     *
489
     * @return $this
490
     */
491
    public function addItemClass(string $class)
492
    {
493
        $this->applyToAll(function (HasHtmlAttributes $link) use ($class) {
494
            $link->addClass($class);
495
        });
496
497
        return $this;
498
    }
499
500
    /**
501
     * Set an attribute on all items in the menu.
502
     *
503
     * @param string $attribute
504
     * @param string $value
505
     *
506
     * @return $this
507
     */
508
    public function setItemAttribute(string $attribute, string $value = '')
509
    {
510
        $this->applyToAll(function (HasHtmlAttributes $link) use ($attribute, $value) {
511
            $link->setAttribute($attribute, $value);
512
        });
513
514
        return $this;
515
    }
516
517
    /**
518
     * Add a parent class to all items in the menu.
519
     *
520
     * @param string $class
521
     *
522
     * @return $this
523
     */
524
    public function addItemParentClass(string $class)
525
    {
526
        $this->applyToAll(function (HasParentAttributes $item) use ($class) {
527
            $item->addParentClass($class);
528
        });
529
530
        return $this;
531
    }
532
533
    /**
534
     * Add a parent attribute to all items in the menu.
535
     *
536
     * @param string $attribute
537
     * @param string $value
538
     *
539
     * @return $this
540
     */
541
    public function setItemParentAttribute(string $attribute, string $value = '')
542
    {
543
        $this->applyToAll(function (HasParentAttributes $item) use ($attribute, $value) {
544
            $item->setParentAttribute($attribute, $value);
545
        });
546
547
        return $this;
548
    }
549
550
    /**
551
     * Set tag for items wrapper.
552
     *
553
     * @param string|null $wrapperTagName
554
     * @return $this
555
     */
556
    public function setWrapperTag($wrapperTagName = null)
557
    {
558
        $this->wrapperTagName = $wrapperTagName;
559
560
        return $this;
561
    }
562
563
    /**
564
     * Set tag for items wrapper.
565
     *
566
     * @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...
567
     * @return $this
568
     */
569
    public function withoutWrapperTag()
570
    {
571
        $this->wrapperTagName = null;
572
573
        return $this;
574
    }
575
576
    /**
577
     * Set the parent tag name.
578
     *
579
     * @param string|null $parentTagName
580
     * @return $this
581
     */
582
    public function setParentTag($parentTagName = null)
583
    {
584
        $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...
585
586
        return $this;
587
    }
588
589
    /**
590
     * Render items without a parent tag.
591
     *
592
     * @return $this
593
     */
594
    public function withoutParentTag()
595
    {
596
        $this->parentTagName = null;
597
598
        return $this;
599
    }
600
601
    /**
602
     * Set whether active class should (also) be on link.
603
     *
604
     * @param $activeClassOnLink
605
     * @return $this
606
     */
607
    public function setActiveClassOnLink(bool $activeClassOnLink = true)
608
    {
609
        $this->activeClassOnLink = $activeClassOnLink;
610
611
        return $this;
612
    }
613
614
    /**
615
     * Set whether active class should (also) be on parent.
616
     *
617
     * @param $activeClassOnParent
618
     * @return $this
619
     */
620
    public function setActiveClassOnParent(bool $activeClassOnParent = true)
621
    {
622
        $this->activeClassOnParent = $activeClassOnParent;
623
624
        return $this;
625
    }
626
627
    /**
628
     * @param bool $condition
629
     * @param callable $callable
630
     *
631
     * @return $this
632
     */
633
    public function if(bool $condition, callable $callable)
634
    {
635
        return $condition ? $callable($this) : $this;
636
    }
637
638
    /**
639
     * Create a empty blueprint of the menu (copies `filters` and `activeClass`).
640
     *
641
     * @return static
642
     */
643
    public function blueprint()
644
    {
645
        $clone = new static();
646
647
        $clone->filters = $this->filters;
648
        $clone->activeClass = $this->activeClass;
649
650
        return $clone;
651
    }
652
653
    /**
654
     * Render the menu.
655
     *
656
     * @return string
657
     */
658
    public function render(): string
659
    {
660
        $tag = $this->wrapperTagName
661
            ? new Tag($this->wrapperTagName, $this->htmlAttributes)
662
            : null;
663
664
        $contents = array_map([$this, 'renderItem'], $this->items);
665
666
        $wrappedContents = $tag ? $tag->withContents($contents) : implode('', $contents);
667
668
        $menu = $this->prepend.$wrappedContents.$this->append;
669
670
        if (! empty($this->wrap)) {
671
            return Tag::make($this->wrap[0], new Attributes($this->wrap[1]))->withContents($menu);
672
        }
673
674
        return $menu;
675
    }
676
677
    protected function renderItem(Item $item): string
678
    {
679
        $attributes = new Attributes();
680
681
        if (method_exists($item, 'beforeRender')) {
682
            $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...
683
        }
684
685
        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...
686
            return '';
687
        }
688
689
        if ($item->isActive()) {
690
            if ($this->activeClassOnParent) {
691
                $attributes->addClass($this->activeClass);
692
            }
693
694
            if ($this->activeClassOnLink && $item instanceof HasHtmlAttributes) {
695
                $item->addClass($this->activeClass);
696
            }
697
        }
698
699
        if ($item instanceof HasParentAttributes) {
700
            $attributes->setAttributes($item->parentAttributes());
701
        }
702
703
        if (! $this->parentTagName) {
704
            return $item->render();
705
        }
706
707
        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...
708
    }
709
710
    /**
711
     * The amount of items in the menu.
712
     *
713
     * @return int
714
     */
715
    public function count(): int
716
    {
717
        return count($this->items);
718
    }
719
720
    /**
721
     * @return string
722
     */
723
    public function __toString(): string
724
    {
725
        return $this->render();
726
    }
727
728
    public function getIterator(): Traversable
729
    {
730
        return new ArrayIterator($this->items);
731
    }
732
}
733