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 (#64)
by Mark
01:25
created

Menu   D

Complexity

Total Complexity 68

Size/Duplication

Total Lines 673
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 13
dl 0
loc 673
rs 4.5034
c 0
b 0
f 0

40 Methods

Rating   Name   Duplication   Size   Complexity  
A setActiveClass() 0 6 1
A addItemClass() 0 8 1
A setItemAttribute() 0 8 1
A addItemParentClass() 0 8 1
A setItemParentAttribute() 0 8 1
A __construct() 0 7 1
A new() 0 4 1
A build() 0 4 2
A fill() 0 10 3
A add() 0 10 2
A addIf() 0 8 2
A link() 0 4 1
A empty() 0 4 1
A linkIf() 0 8 2
A html() 0 4 1
A htmlIf() 0 8 2
A submenu() 0 9 1
A submenuIf() 0 8 2
A parseSubmenuArgs() 0 8 2
A createSubmenuMenu() 0 10 2
A createSubmenuHeader() 0 8 2
A each() 0 14 3
A registerFilter() 0 6 1
A applyFilter() 0 10 2
A applyToAll() 0 7 1
A wrap() 0 6 1
A isActive() 0 10 3
A setActive() 0 12 3
A setActiveFromUrl() 0 12 1
A setActiveFromCallable() 0 20 3
A setTagName() 0 6 1
A wrapLinksInList() 0 6 1
A setActiveClassOnLink() 0 6 1
A setActiveClassOnParent() 0 6 1
A if() 0 4 2
A blueprint() 0 9 1
A render() 0 14 2
C renderItem() 0 24 7
A count() 0 4 1
A __toString() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Menu often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Menu, and based on these observations, apply Extract Interface, too.

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