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 ( 769f50...782770 )
by Sebastian
12s
created

Menu   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 65
lcom 1
cbo 12
dl 0
loc 655
rs 5.389
c 0
b 0
f 0

39 Methods

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