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 (#37)
by Roj
04:10
created

Menu::createSubmenuMenu()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
1
<?php
2
3
namespace Spatie\Menu;
4
5
use Closure;
6
use Countable;
7
use Spatie\Menu\Helpers\Arr;
8
use Spatie\HtmlElement\Attributes;
9
use Spatie\HtmlElement\HtmlElement;
10
use Spatie\Menu\Helpers\Reflection;
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;
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\HtmlElement\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);
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 $conditional
193
     * @return bool
194
     */
195
    protected function resolveCondition($conditional)
196
    {
197
        return $conditional instanceof Closure ? $conditional() : $conditional;
198
    }
199
200
    /**
201
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
202
     * @param callable|\Spatie\Menu\Menu|null $menu
203
     *
204
     * @return $this
205
     */
206
    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...
207
    {
208
        list($header, $menu) = $this->parseSubmenuArgs(func_get_args());
209
210
        $menu = $this->createSubmenuMenu($menu);
211
        $header = $this->createSubmenuHeader($header);
212
213
        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...
214
    }
215
216
    /**
217
     * @param bool $condition
218
     * @param callable|\Spatie\Menu\Menu|\Spatie\Menu\Item $header
219
     * @param callable|\Spatie\Menu\Menu|null $menu
220
     *
221
     * @return $this
222
     */
223
    public function submenuIf($condition, $header, $menu = null)
224
    {
225
        if ($condition) {
226
            $this->submenu($header, $menu);
227
        }
228
229
        return $this;
230
    }
231
232
    protected function parseSubmenuArgs($args): array
233
    {
234
        if (count($args) === 1) {
235
            return ['', $args[0]];
236
        }
237
238
        return [$args[0], $args[1]];
239
    }
240
241
    /**
242
     * @param \Spatie\Menu\Menu|callable $menu
243
     *
244
     * @return \Spatie\Menu\Menu
245
     */
246
    protected function createSubmenuMenu($menu): Menu
247
    {
248
        if (is_callable($menu)) {
249
            $transformer = $menu;
250
            $menu = $this->blueprint();
251
            $transformer($menu);
252
        }
253
254
        return $menu;
255
    }
256
257
    /**
258
     * @param \Spatie\Menu\Item|string $header
259
     *
260
     * @return string
261
     */
262
    protected function createSubmenuHeader($header): string
263
    {
264
        if ($header instanceof Item) {
265
            $header = $header->render();
266
        }
267
268
        return $header;
269
    }
270
271
    /**
272
     * Iterate over all the items and apply a callback. If you typehint the
273
     * item parameter in the callable, it wil only be applied to items of that
274
     * type.
275
     *
276
     * @param callable $callable
277
     *
278
     * @return $this
279
     */
280
    public function each(callable $callable)
281
    {
282
        $type = Reflection::firstParameterType($callable);
283
284
        foreach ($this->items as $item) {
285
            if (! Reflection::itemMatchesType($item, $type)) {
286
                continue;
287
            }
288
289
            $callable($item);
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * Register a filter to the menu. When an item is added, all filters will be
297
     * applied to the item. If you typehint the item parameter in the callable, it
298
     * will only be applied to items of that type.
299
     *
300
     * @param callable $callable
301
     *
302
     * @return $this
303
     */
304
    public function registerFilter(callable $callable)
305
    {
306
        $this->filters[] = $callable;
307
308
        return $this;
309
    }
310
311
    /**
312
     * Apply a filter to an item. Returns the result of the filter.
313
     *
314
     * @param callable          $filter
315
     * @param \Spatie\Menu\Item $item
316
     */
317
    protected function applyFilter(callable $filter, Item $item)
318
    {
319
        $type = Reflection::firstParameterType($filter);
320
321
        if (! Reflection::itemMatchesType($item, $type)) {
322
            return;
323
        }
324
325
        $filter($item);
326
    }
327
328
    /**
329
     * Apply a callable to all existing items, and register it as a filter so it
330
     * will get applied to all new items too. If you typehint the item parameter
331
     * in the callable, it wil only be applied to items of that type.
332
     *
333
     * @param callable $callable
334
     *
335
     * @return $this
336
     */
337
    public function applyToAll(callable $callable)
338
    {
339
        $this->each($callable);
340
        $this->registerFilter($callable);
341
342
        return $this;
343
    }
344
345
    /**
346
     * Prepend the menu with a string of html on render.
347
     *
348
     * @param string $prepend
349
     *
350
     * @return $this
351
     */
352
    public function prepend(string $prepend)
353
    {
354
        $this->prepend = $prepend;
355
356
        return $this;
357
    }
358
359
    /**
360
     * Prepend the menu with a string of html on render if a certain condition is
361
     * met.
362
     *
363
     * @param bool   $condition
364
     * @param string $prepend
365
     *
366
     * @return $this
367
     */
368
    public function prependIf($condition, string $prepend)
369
    {
370
        if ($this->resolveCondition($condition)) {
371
            return $this->prepend($prepend);
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Append a string of html to the menu on render.
379
     *
380
     * @param string $append
381
     *
382
     * @return $this
383
     */
384
    public function append(string $append)
385
    {
386
        $this->append = $append;
387
388
        return $this;
389
    }
390
391
    /**
392
     * Append the menu with a string of html on render if a certain condition is
393
     * met.
394
     *
395
     * @param bool   $condition
396
     * @param string $append
397
     *
398
     * @return static
399
     */
400
    public function appendIf($condition, string $append)
401
    {
402
        if ($this->resolveCondition($condition)) {
403
            return $this->append($append);
404
        }
405
406
        return $this;
407
    }
408
409
    /**
410
     * Wrap the menu in an html element.
411
     *
412
     * @param string $element
413
     * @param array $attributes
414
     *
415
     * @return $this
416
     */
417
    public function wrap(string $element, $attributes = [])
418
    {
419
        $this->wrap = [$element, $attributes];
420
421
        return $this;
422
    }
423
424
    /**
425
     * Determine whether the menu is active.
426
     *
427
     * @return bool
428
     */
429
    public function isActive(): bool
430
    {
431
        foreach ($this->items as $item) {
432
            if ($item->isActive()) {
433
                return true;
434
            }
435
        }
436
437
        return false;
438
    }
439
440
    /**
441
     * Set multiple items in the menu as active based on a callable that filters
442
     * through items. If you typehint the item parameter in the callable, it will
443
     * only be applied to items of that type.
444
     *
445
     * @param callable|string $urlOrCallable
446
     * @param string          $root
447
     *
448
     * @return $this
449
     */
450
    public function setActive($urlOrCallable, string $root = '/')
451
    {
452
        if (is_string($urlOrCallable)) {
453
            return $this->setActiveFromUrl($urlOrCallable, $root);
454
        }
455
456
        if (is_callable($urlOrCallable)) {
457
            return $this->setActiveFromCallable($urlOrCallable);
458
        }
459
460
        throw new \InvalidArgumentException('`setActive` requires a pattern or a callable');
461
    }
462
463
    /**
464
     * Set all relevant children active based on the current request's URL.
465
     *
466
     * /, /about, /contact => request to /about will set the about link active.
467
     *
468
     * /en, /en/about, /en/contact => request to /en won't set /en active if the
469
     *                                request root is set to /en.
470
     *
471
     * @param string $url  The current request url.
472
     * @param string $root If the link's URL is an exact match with the request
473
     *                     root, the link won't be set active. This behavior is
474
     *                     to avoid having home links active on every request.
475
     *
476
     * @return $this
477
     */
478
    public function setActiveFromUrl(string $url, string $root = '/')
479
    {
480
        $this->applyToAll(function (Menu $menu) use ($url, $root) {
481
            $menu->setActiveFromUrl($url, $root);
482
        });
483
484
        $this->applyToAll(function (Activatable $item) use ($url, $root) {
485
            $item->determineActiveForUrl($url, $root);
486
        });
487
488
        return $this;
489
    }
490
491
    /**
492
     * @param callable $callable
493
     *
494
     * @return $this
495
     */
496
    public function setActiveFromCallable(callable $callable)
497
    {
498
        $this->applyToAll(function (Menu $menu) use ($callable) {
499
            $menu->setActiveFromCallable($callable);
500
        });
501
502
        $type = Reflection::firstParameterType($callable);
503
504
        $this->applyToAll(function (Activatable $item) use ($callable, $type) {
505
            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...
506
                return;
507
            }
508
509
            if ($callable($item)) {
510
                $item->setActive();
511
            }
512
        });
513
514
        return $this;
515
    }
516
517
    /**
518
     * Set the class name that will be used on active items for this menu.
519
     *
520
     * @param string $class
521
     *
522
     * @return $this
523
     */
524
    public function setActiveClass(string $class)
525
    {
526
        $this->activeClass = $class;
527
528
        return $this;
529
    }
530
531
    /**
532
     * Add a class to all items in the menu.
533
     *
534
     * @param string $class
535
     *
536
     * @return $this
537
     */
538
    public function addItemClass(string $class)
539
    {
540
        $this->applyToAll(function (HasHtmlAttributes $link) use ($class) {
541
            $link->addClass($class);
542
        });
543
544
        return $this;
545
    }
546
547
    /**
548
     * Set an attribute on all items in the menu.
549
     *
550
     * @param string $attribute
551
     * @param string $value
552
     *
553
     * @return $this
554
     */
555
    public function setItemAttribute(string $attribute, string $value = '')
556
    {
557
        $this->applyToAll(function (HasHtmlAttributes $link) use ($attribute, $value) {
558
            $link->setAttribute($attribute, $value);
559
        });
560
561
        return $this;
562
    }
563
564
    /**
565
     * Add a parent class to all items in the menu.
566
     *
567
     * @param string $class
568
     *
569
     * @return $this
570
     */
571
    public function addItemParentClass(string $class)
572
    {
573
        $this->applyToAll(function (HasParentAttributes $item) use ($class) {
574
            $item->addParentClass($class);
575
        });
576
577
        return $this;
578
    }
579
580
    /**
581
     * Add a parent attribute to all items in the menu.
582
     *
583
     * @param string $attribute
584
     * @param string $value
585
     *
586
     * @return $this
587
     */
588
    public function setItemParentAttribute(string $attribute, string $value = '')
589
    {
590
        $this->applyToAll(function (HasParentAttributes $item) use ($attribute, $value) {
591
            $item->setParentAttribute($attribute, $value);
592
        });
593
594
        return $this;
595
    }
596
597
    /**
598
     * @param bool $condition
599
     * @param callable $callable
600
     *
601
     * @return $this
602
     */
603
    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...
604
    {
605
        return $condition ? $callable($this) : $this;
606
    }
607
608
    /**
609
     * Create a empty blueprint of the menu (copies `filters` and `activeClass`).
610
     *
611
     * @return static
612
     */
613
    public function blueprint()
614
    {
615
        $clone = new static();
616
617
        $clone->filters = $this->filters;
618
        $clone->activeClass = $this->activeClass;
619
620
        return $clone;
621
    }
622
623
    /**
624
     * Render the menu.
625
     *
626
     * @return string
627
     */
628
    public function render(): string
629
    {
630
        $contents = HtmlElement::render(
631
            'ul',
632
            $this->htmlAttributes->toArray(),
633
            Arr::map($this->items, function (Item $item) {
634
                return HtmlElement::render(
635
                    $item->isActive() ? "li.{$this->activeClass}" : 'li',
636
                    $item instanceof HasParentAttributes ? $item->parentAttributes() : [],
637
                    $item->render()
638
                );
639
            })
640
        );
641
642
        $menu = "{$this->prepend}{$contents}{$this->append}";
643
644
        if (! empty($this->wrap)) {
645
            return HtmlElement::render($this->wrap[0], $this->wrap[1], $menu);
646
        }
647
648
        return $menu;
649
    }
650
651
    /**
652
     * The amount of items in the menu.
653
     *
654
     * @return int
655
     */
656
    public function count(): int
657
    {
658
        return count($this->items);
659
    }
660
661
    /**
662
     * @return string
663
     */
664
    public function __toString(): string
665
    {
666
        return $this->render();
667
    }
668
}
669