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
Pull Request — master (#44)
by
unknown
03:14 queued 01:49
created

Menu::each()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
c 0
b 0
f 0
rs 9.4285
cc 3
eloc 7
nc 3
nop 1
1
<?php
2
3
namespace Spatie\Menu;
4
5
use Countable;
6
use Spatie\Menu\Html\Tag;
7
use Spatie\Menu\Html\Attributes;
8
use Spatie\Menu\Helpers\Reflection;
9
use Spatie\Menu\Traits\Conditions as ConditionsTrait;
10
use Spatie\Menu\Traits\HasTextAttributes as HasAttributesTrait;
11
use Spatie\Menu\Traits\HasHtmlAttributes as HasHtmlAttributesTrait;
12
use Spatie\Menu\Traits\HasParentAttributes as HasParentAttributesTrait;
13
14
class Menu implements Item, Countable, HasHtmlAttributes, HasParentAttributes
15
{
16
    use HasHtmlAttributesTrait, HasParentAttributesTrait, ConditionsTrait, HasAttributesTrait;
17
18
    /** @var array */
19
    protected $items = [];
20
21
    /** @var array */
22
    protected $filters = [];
23
24
    /** @var string */
25
    protected $prepend, $append = '';
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
26
27
    /** @var array */
28
    protected $wrap = [];
29
30
    /** @var string */
31
    protected $activeClass = 'active';
32
33
    /** @var \Spatie\Menu\Html\Attributes */
34
    protected $htmlAttributes, $parentAttributes;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
35
36
    protected function __construct(Item ...$items)
37
    {
38
        $this->items = $items;
39
40
        $this->htmlAttributes = new Attributes();
41
        $this->parentAttributes = new Attributes();
42
    }
43
44
    /**
45
     * Create a new menu, optionally prefilled with items.
46
     *
47
     * @param array $items
48
     *
49
     * @return static
50
     */
51
    public static function new($items = [])
52
    {
53
        return new static(...array_values($items));
54
    }
55
56
    /**
57
     * Build a new menu from an array. The callback receives a menu instance as
58
     * the accumulator, the array item as the second parameter, and the item's
59
     * key as the third.
60
     *
61
     * @param array|\Iterator $items
62
     * @param callable $callback
63
     * @param \Spatie\Menu\Menu|null $initial
64
     *
65
     * @return static
66
     */
67
    public static function build($items, callable $callback, Menu $initial = null)
68
    {
69
        return ($initial ?: static::new())->fill($items, $callback);
70
    }
71
72
    /**
73
     * Fill a menu from an array. The callback receives a menu instance as
74
     * the accumulator, the array item as the second parameter, and the item's
75
     * key as the third.
76
     *
77
     * @param array|\Iterator $items
78
     * @param callable $callback
79
     *
80
     * @return static
81
     */
82
    public function fill($items, callable $callback)
83
    {
84
        $menu = $this;
85
86
        foreach ($items as $key => $item) {
87
            $menu = $callback($menu, $item, $key) ?: $menu;
88
        }
89
90
        return $menu;
91
    }
92
93
    /**
94
     * Add an item to the menu. This also applies all registered filters to the
95
     * item.
96
     *
97
     * @param \Spatie\Menu\Item $item
98
     *
99
     * @return $this
100
     */
101
    public function add(Item $item)
102
    {
103
        foreach ($this->filters as $filter) {
104
            $this->applyFilter($filter, $item);
105
        }
106
107
        $this->items[] = $item;
108
109
        return $this;
110
    }
111
112
    /**
113
     * Add an item to the menu if a (non-strict) condition is met.
114
     *
115
     * @param bool              $condition
116
     * @param \Spatie\Menu\Item $item
117
     *
118
     * @return $this
119
     */
120
    public function addIf($condition, Item $item)
121
    {
122
        if ($this->resolveCondition($condition)) {
123
            $this->add($item);
124
        }
125
126
        return $this;
127
    }
128
129
    /**
130
     * Shortcut function to add a plain link to the menu.
131
     *
132
     * @param string $url
133
     * @param string $text
134
     *
135
     * @return $this
136
     */
137
    public function link(string $url, string $text)
138
    {
139
        return $this->add(Link::to($url, $text));
140
    }
141
142
    /**
143
     * Add a link to the menu if a (non-strict) condition is met.
144
     *
145
     * @param bool   $condition
146
     * @param string $url
147
     * @param string $text
148
     *
149
     * @return $this
150
     */
151
    public function linkIf($condition, string $url, string $text)
152
    {
153
        if ($this->resolveCondition($condition)) {
154
            $this->link($url, $text);
155
        }
156
157
        return $this;
158
    }
159
160
    /**
161
     * Shortcut function to add raw html to the menu.
162
     *
163
     * @param string $html
164
     * @param array  $parentAttributes
165
     *
166
     * @return $this
167
     */
168
    public function html(string $html, array $parentAttributes = [])
169
    {
170
        return $this->add(Html::raw($html)->setParentAttributes($parentAttributes));
171
    }
172
173
    /**
174
     * Add a chunk of html if a (non-strict) condition is met.
175
     *
176
     * @param bool   $condition
177
     * @param string $html
178
     * @param array  $parentAttributes
179
     *
180
     * @return $this
181
     */
182
    public function htmlIf($condition, string $html, array $parentAttributes = [])
183
    {
184
        if ($this->resolveCondition($condition)) {
185
            $this->html($html, $parentAttributes);
186
        }
187
188
        return $this;
189
    }
190
191
    /**
192
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
193
     * @param callable|\Spatie\Menu\Menu|null $menu
194
     *
195
     * @return $this
196
     */
197
    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...
198
    {
199
        list($header, $menu) = $this->parseSubmenuArgs(func_get_args());
200
201
        $menu = $this->createSubmenuMenu($menu);
202
        $header = $this->createSubmenuHeader($header);
203
204
        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...
205
    }
206
207
    /**
208
     * @param bool $condition
209
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
210
     * @param callable|\Spatie\Menu\Menu|null $menu
211
     *
212
     * @return $this
213
     */
214
    public function submenuIf($condition, $header, $menu = null)
215
    {
216
        if ($condition) {
217
            $this->submenu($header, $menu);
218
        }
219
220
        return $this;
221
    }
222
223
    protected function parseSubmenuArgs($args): array
224
    {
225
        if (count($args) === 1) {
226
            return ['', $args[0]];
227
        }
228
229
        return [$args[0], $args[1]];
230
    }
231
232
    /**
233
     * @param \Spatie\Menu\Menu|callable $menu
234
     *
235
     * @return \Spatie\Menu\Menu
236
     */
237
    protected function createSubmenuMenu($menu): Menu
238
    {
239
        if (is_callable($menu)) {
240
            $transformer = $menu;
241
            $menu = $this->blueprint();
242
            $transformer($menu);
243
        }
244
245
        return $menu;
246
    }
247
248
    /**
249
     * @param \Spatie\Menu\Item|string $header
250
     *
251
     * @return string
252
     */
253
    protected function createSubmenuHeader($header): string
254
    {
255
        if ($header instanceof Item) {
256
            $header = $header->render();
257
        }
258
259
        return $header;
260
    }
261
262
    /**
263
     * Iterate over all the items and apply a callback. If you typehint the
264
     * item parameter in the callable, it wil only be applied to items of that
265
     * type.
266
     *
267
     * @param callable $callable
268
     *
269
     * @return $this
270
     */
271
    public function each(callable $callable)
272
    {
273
        $type = Reflection::firstParameterType($callable);
274
275
        foreach ($this->items as $item) {
276
            if (! Reflection::itemMatchesType($item, $type)) {
277
                continue;
278
            }
279
280
            $callable($item);
281
        }
282
283
        return $this;
284
    }
285
286
    /**
287
     * Register a filter to the menu. When an item is added, all filters will be
288
     * applied to the item. If you typehint the item parameter in the callable, it
289
     * will only be applied to items of that type.
290
     *
291
     * @param callable $callable
292
     *
293
     * @return $this
294
     */
295
    public function registerFilter(callable $callable)
296
    {
297
        $this->filters[] = $callable;
298
299
        return $this;
300
    }
301
302
    /**
303
     * Apply a filter to an item. Returns the result of the filter.
304
     *
305
     * @param callable          $filter
306
     * @param \Spatie\Menu\Item $item
307
     */
308
    protected function applyFilter(callable $filter, Item $item)
309
    {
310
        $type = Reflection::firstParameterType($filter);
311
312
        if (! Reflection::itemMatchesType($item, $type)) {
313
            return;
314
        }
315
316
        $filter($item);
317
    }
318
319
    /**
320
     * Apply a callable to all existing items, and register it as a filter so it
321
     * will get applied to all new items too. If you typehint the item parameter
322
     * in the callable, it wil only be applied to items of that type.
323
     *
324
     * @param callable $callable
325
     *
326
     * @return $this
327
     */
328
    public function applyToAll(callable $callable)
329
    {
330
        $this->each($callable);
331
        $this->registerFilter($callable);
332
333
        return $this;
334
    }
335
336
    /**
337
     * Wrap the menu in an html element.
338
     *
339
     * @param string $element
340
     * @param array $attributes
341
     *
342
     * @return $this
343
     */
344
    public function wrap(string $element, $attributes = [])
345
    {
346
        $this->wrap = [$element, $attributes];
347
348
        return $this;
349
    }
350
351
    /**
352
     * Determine whether the menu is active.
353
     *
354
     * @return bool
355
     */
356
    public function isActive(): bool
357
    {
358
        foreach ($this->items as $item) {
359
            if ($item->isActive()) {
360
                return true;
361
            }
362
        }
363
364
        return false;
365
    }
366
367
    /**
368
     * Set multiple items in the menu as active based on a callable that filters
369
     * through items. If you typehint the item parameter in the callable, it will
370
     * only be applied to items of that type.
371
     *
372
     * @param callable|string $urlOrCallable
373
     * @param string          $root
374
     *
375
     * @return $this
376
     */
377
    public function setActive($urlOrCallable, string $root = '/')
378
    {
379
        if (is_string($urlOrCallable)) {
380
            return $this->setActiveFromUrl($urlOrCallable, $root);
381
        }
382
383
        if (is_callable($urlOrCallable)) {
384
            return $this->setActiveFromCallable($urlOrCallable);
385
        }
386
387
        throw new \InvalidArgumentException('`setActive` requires a pattern or a callable');
388
    }
389
390
    /**
391
     * Set all relevant children active based on the current request's URL.
392
     *
393
     * /, /about, /contact => request to /about will set the about link active.
394
     *
395
     * /en, /en/about, /en/contact => request to /en won't set /en active if the
396
     *                                request root is set to /en.
397
     *
398
     * @param string $url  The current request url.
399
     * @param string $root If the link's URL is an exact match with the request
400
     *                     root, the link won't be set active. This behavior is
401
     *                     to avoid having home links active on every request.
402
     *
403
     * @return $this
404
     */
405
    public function setActiveFromUrl(string $url, string $root = '/')
406
    {
407
        $this->applyToAll(function (Menu $menu) use ($url, $root) {
408
            $menu->setActiveFromUrl($url, $root);
409
        });
410
411
        $this->applyToAll(function (Activatable $item) use ($url, $root) {
412
            $item->determineActiveForUrl($url, $root);
413
        });
414
415
        return $this;
416
    }
417
418
    /**
419
     * @param callable $callable
420
     *
421
     * @return $this
422
     */
423
    public function setActiveFromCallable(callable $callable)
424
    {
425
        $this->applyToAll(function (Menu $menu) use ($callable) {
426
            $menu->setActiveFromCallable($callable);
427
        });
428
429
        $type = Reflection::firstParameterType($callable);
430
431
        $this->applyToAll(function (Activatable $item) use ($callable, $type) {
432
            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...
433
                return;
434
            }
435
436
            if ($callable($item)) {
437
                $item->setActive();
438
            }
439
        });
440
441
        return $this;
442
    }
443
444
    /**
445
     * Set the class name that will be used on active items for this menu.
446
     *
447
     * @param string $class
448
     *
449
     * @return $this
450
     */
451
    public function setActiveClass(string $class)
452
    {
453
        $this->activeClass = $class;
454
455
        return $this;
456
    }
457
458
    /**
459
     * Add a class to all items in the menu.
460
     *
461
     * @param string $class
462
     *
463
     * @return $this
464
     */
465
    public function addItemClass(string $class)
466
    {
467
        $this->applyToAll(function (HasHtmlAttributes $link) use ($class) {
468
            $link->addClass($class);
469
        });
470
471
        return $this;
472
    }
473
474
    /**
475
     * Set an attribute on all items in the menu.
476
     *
477
     * @param string $attribute
478
     * @param string $value
479
     *
480
     * @return $this
481
     */
482
    public function setItemAttribute(string $attribute, string $value = '')
483
    {
484
        $this->applyToAll(function (HasHtmlAttributes $link) use ($attribute, $value) {
485
            $link->setAttribute($attribute, $value);
486
        });
487
488
        return $this;
489
    }
490
491
    /**
492
     * Add a parent class to all items in the menu.
493
     *
494
     * @param string $class
495
     *
496
     * @return $this
497
     */
498
    public function addItemParentClass(string $class)
499
    {
500
        $this->applyToAll(function (HasParentAttributes $item) use ($class) {
501
            $item->addParentClass($class);
502
        });
503
504
        return $this;
505
    }
506
507
    /**
508
     * Add a parent attribute to all items in the menu.
509
     *
510
     * @param string $attribute
511
     * @param string $value
512
     *
513
     * @return $this
514
     */
515
    public function setItemParentAttribute(string $attribute, string $value = '')
516
    {
517
        $this->applyToAll(function (HasParentAttributes $item) use ($attribute, $value) {
518
            $item->setParentAttribute($attribute, $value);
519
        });
520
521
        return $this;
522
    }
523
524
    /**
525
     * @param bool $condition
526
     * @param callable $callable
527
     *
528
     * @return $this
529
     */
530
    public function if(bool $condition, callable $callable)
0 ignored issues
show
Coding Style introduced by
Possible parse error: non-abstract method defined as abstract
Loading history...
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
531
    {
532
        return $condition ? $callable($this) : $this;
533
    }
534
535
    /**
536
     * Create a empty blueprint of the menu (copies `filters` and `activeClass`).
537
     *
538
     * @return static
539
     */
540
    public function blueprint()
541
    {
542
        $clone = new static();
543
544
        $clone->filters = $this->filters;
545
        $clone->activeClass = $this->activeClass;
546
547
        return $clone;
548
    }
549
550
    /**
551
     * Render the menu.
552
     *
553
     * @return string
554
     */
555
    public function render(): string
556
    {
557
        $tag = new Tag('ul', $this->htmlAttributes);
558
559
        $contents = array_map([$this, 'renderItem'], $this->items);
560
561
        $menu = $this->prepend.$tag->withContents($contents).$this->append;
562
563
        if (! empty($this->wrap)) {
564
            return Tag::make($this->wrap[0], new Attributes($this->wrap[1]))->withContents($menu);
565
        }
566
567
        return $menu;
568
    }
569
570
    protected function renderItem(Item $item): string
571
    {
572
        $attributes = new Attributes();
573
574
        if ($item->isActive()) {
575
            $attributes->addClass($this->activeClass);
576
        }
577
578
        if ($item instanceof HasParentAttributes) {
579
            $attributes->setAttributes($item->parentAttributes());
580
        }
581
582
        return Tag::make('li', $attributes)->withContents($item->render());
583
    }
584
585
    /**
586
     * The amount of items in the menu.
587
     *
588
     * @return int
589
     */
590
    public function count(): int
591
    {
592
        return count($this->items);
593
    }
594
595
    /**
596
     * @return string
597
     */
598
    public function __toString(): string
599
    {
600
        return $this->render();
601
    }
602
}
603