MenuGenerator   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 456
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Importance

Changes 0
Metric Value
wmc 47
lcom 2
cbo 5
dl 0
loc 456
rs 8.64
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A findBy() 0 10 3
A findByTitleOrAdd() 0 9 3
A setViewFactory() 0 6 1
A setView() 0 6 1
A setUrlPrefix() 0 6 1
A setPresenter() 0 6 1
A getPresenter() 0 4 1
A presenterExists() 0 4 1
A setBindings() 0 6 1
B resolve() 0 21 6
A resolveItems() 0 10 2
A add() 0 7 1
A dropdown() 0 8 1
A route() 0 6 1
A url() 0 7 2
A header() 0 6 1
A divider() 0 6 1
A count() 0 4 1
A destroy() 0 6 1
A getOrderedItems() 0 10 3
A render() 0 12 4
A renderView() 0 4 1
B renderMenu() 0 25 6
A formatUrl() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like MenuGenerator 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 MenuGenerator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Menus\Models;
6
7
use Countable;
8
use Illuminate\Support\Collection;
9
use Illuminate\View\Factory as ViewFactory;
10
use Rinvex\Menus\Presenters\NavbarPresenter;
11
use Rinvex\Menus\Contracts\PresenterContract;
12
use Illuminate\Contracts\View\View as ViewContract;
13
14
class MenuGenerator implements Countable
15
{
16
    /**
17
     * The items collection.
18
     *
19
     * @var \Illuminate\Support\Collection
20
     */
21
    protected $items;
22
23
    /**
24
     * The presenter class.
25
     *
26
     * @var string
27
     */
28
    protected $presenter = NavbarPresenter::class;
29
30
    /**
31
     * The URL prefix.
32
     *
33
     * @var string|null
34
     */
35
    protected $urlPrefix;
36
37
    /**
38
     * The view name.
39
     *
40
     * @var string
41
     */
42
    protected $view;
43
44
    /**
45
     * The laravel view factory instance.
46
     *
47
     * @var \Illuminate\View\Factory
48
     */
49
    protected $views;
50
51
    /**
52
     * Resolved item binding map.
53
     *
54
     * @var array
55
     */
56
    protected $bindings = [];
57
58
    /**
59
     * Create a new MenuGenerator instance.
60
     */
61
    public function __construct()
62
    {
63
        $this->items = collect();
64
    }
65
66
    /**
67
     * Find menu item by given key and value.
68
     *
69
     * @param string   $key
70
     * @param string   $value
71
     * @param callable $callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $callback not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
72
     *
73
     * @return \Rinvex\Menus\Models\MenuItem|null
74
     */
75
    public function findBy(string $key, string $value, callable $callback = null): ?MenuItem
76
    {
77
        $item = $this->items->filter(function ($item) use ($key, $value) {
78
            return $item->{$key} === $value;
79
        })->first();
80
81
        (! is_callable($callback) || ! $item) || call_user_func($callback, $item);
82
83
        return $item;
84
    }
85
86
    /**
87
     * Find menu item by given key and value.
88
     *
89
     * @param string   $title
90
     * @param int      $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
91
     * @param string   $icon
0 ignored issues
show
Documentation introduced by
Should the type for parameter $icon not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
92
     * @param string   $type
0 ignored issues
show
Documentation introduced by
Should the type for parameter $type not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
93
     * @param array    $attributes
94
     * @param callable $callback
0 ignored issues
show
Documentation introduced by
Should the type for parameter $callback not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
95
     *
96
     * @return \Rinvex\Menus\Models\MenuItem|null
97
     */
98
    public function findByTitleOrAdd(string $title, int $order = null, string $icon = null, string $type = null, array $attributes = [], callable $callback = null): ?MenuItem
99
    {
100
        if (! ($item = $this->findBy('title', $title, $callback))) {
101
            $item = $this->add(compact('type', 'title', 'order', 'icon', 'attributes'));
102
            ! is_callable($callback) || call_user_func($callback, $item);
103
        }
104
105
        return $item;
106
    }
107
108
    /**
109
     * Set view factory instance.
110
     *
111
     * @param \Illuminate\View\Factory $views
112
     *
113
     * @return $this
114
     */
115
    public function setViewFactory(ViewFactory $views)
116
    {
117
        $this->views = $views;
118
119
        return $this;
120
    }
121
122
    /**
123
     * Set view.
124
     *
125
     * @param string $view
126
     *
127
     * @return $this
128
     */
129
    public function setView(string $view)
130
    {
131
        $this->view = $view;
132
133
        return $this;
134
    }
135
136
    /**
137
     * Set Prefix URL.
138
     *
139
     * @param string $prefixUrl
0 ignored issues
show
Bug introduced by
There is no parameter named $prefixUrl. 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...
140
     *
141
     * @return $this
142
     */
143
    public function setUrlPrefix(string $urlPrefix)
144
    {
145
        $this->urlPrefix = $urlPrefix;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Set new presenter class.
152
     *
153
     * @param string $presenter
154
     *
155
     * @return $this
156
     */
157
    public function setPresenter(string $presenter)
158
    {
159
        $this->presenter = app('rinvex.menus.presenters')->get($presenter);
160
161
        return $this;
162
    }
163
164
    /**
165
     * Get presenter instance.
166
     *
167
     * @return \Rinvex\Menus\Contracts\PresenterContract
168
     */
169
    public function getPresenter(): PresenterContract
170
    {
171
        return new $this->presenter();
172
    }
173
174
    /**
175
     * Determine if the given name in the presenter style.
176
     *
177
     * @param string $presenter
178
     *
179
     * @return bool
180
     */
181
    public function presenterExists(string $presenter): bool
182
    {
183
        return app('rinvex.menus.presenters')->has($presenter);
184
    }
185
186
    /**
187
     * Set the resolved item bindings.
188
     *
189
     * @param array $bindings
190
     *
191
     * @return $this
192
     */
193
    public function setBindings(array $bindings)
194
    {
195
        $this->bindings = $bindings;
196
197
        return $this;
198
    }
199
200
    /**
201
     * Resolves a key from the bindings array.
202
     *
203
     * @param string|array $key
204
     *
205
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
206
     */
207
    public function resolve($key)
208
    {
209
        if (is_array($key)) {
210
            foreach ($key as $k => $v) {
211
                $key[$k] = $this->resolve($v);
212
            }
213
        } elseif (is_string($key)) {
214
            $matches = [];
215
216
            // Search for any {placeholders} and replace with their replacement values
217
            preg_match_all('/{[\s]*?([^\s]+)[\s]*?}/i', $key, $matches, PREG_SET_ORDER);
218
219
            foreach ($matches as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
220
                if (array_key_exists($match[1], $this->bindings)) {
221
                    $key = preg_replace('/'.$match[0].'/', $this->bindings[$match[1]], $key, 1);
222
                }
223
            }
224
        }
225
226
        return $key;
227
    }
228
229
    /**
230
     * Resolves an array of menu items properties.
231
     *
232
     * @param \Illuminate\Support\Collection &$items
233
     *
234
     * @return void
235
     */
236
    protected function resolveItems(Collection &$items): void
237
    {
238
        $resolver = function ($property) {
239
            return $this->resolve($property) ?: $property;
240
        };
241
242
        $items->each(function (MenuItem $item) use ($resolver) {
243
            $item->fill(array_map($resolver, $item->properties));
244
        });
245
    }
246
247
    /**
248
     * Add new child menu.
249
     *
250
     * @param array $properties
251
     *
252
     * @return \Rinvex\Menus\Models\MenuItem
253
     */
254
    protected function add(array $properties = []): MenuItem
255
    {
256
        $properties['attributes']['id'] = $properties['attributes']['id'] ?? md5(json_encode($properties));
257
        $this->items->push($item = new MenuItem($properties));
258
259
        return $item;
260
    }
261
262
    /**
263
     * Create new menu with dropdown.
264
     *
265
     * @param callable $callback
266
     * @param string   $title
267
     * @param int      $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
268
     * @param string   $icon
0 ignored issues
show
Documentation introduced by
Should the type for parameter $icon not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
269
     * @param array    $attributes
270
     *
271
     * @return \Rinvex\Menus\Models\MenuItem
272
     */
273
    public function dropdown(callable $callback, string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
274
    {
275
        $type = 'dropdown';
276
277
        call_user_func($callback, $item = $this->add(compact('type', 'title', 'order', 'icon', 'attributes')));
278
279
        return $item;
280
    }
281
282
    /**
283
     * Register new menu item using registered route.
284
     *
285
     * @param string $route
0 ignored issues
show
Documentation introduced by
Should the type for parameter $route not be array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
286
     * @param string $title
287
     * @param int    $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
288
     * @param string $icon
0 ignored issues
show
Documentation introduced by
Should the type for parameter $icon not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
289
     * @param array  $attributes
290
     *
291
     * @return \Rinvex\Menus\Models\MenuItem
292
     */
293
    public function route(array $route, string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
294
    {
295
        $type = 'route';
296
297
        return $this->add(compact('type', 'route', 'title', 'order', 'icon', 'attributes'));
298
    }
299
300
    /**
301
     * Register new menu item using url.
302
     *
303
     * @param string $url
304
     * @param string $title
305
     * @param int    $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
306
     * @param string $icon
0 ignored issues
show
Documentation introduced by
Should the type for parameter $icon not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
307
     * @param array  $attributes
308
     *
309
     * @return \Rinvex\Menus\Models\MenuItem
310
     */
311
    public function url(string $url, string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
312
    {
313
        $type = 'url';
314
        ! $this->urlPrefix || $url = $this->formatUrl($url);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->urlPrefix of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
315
316
        return $this->add(compact('type', 'url', 'title', 'order', 'icon', 'attributes'));
317
    }
318
319
    /**
320
     * Add new header item.
321
     *
322
     * @param string $title
323
     * @param int    $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
324
     * @param string $icon
0 ignored issues
show
Documentation introduced by
Should the type for parameter $icon not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
325
     * @param array  $attributes
326
     *
327
     * @return \Rinvex\Menus\Models\MenuItem
328
     */
329
    public function header(string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
330
    {
331
        $type = 'header';
332
333
        return $this->add(compact('type', 'title', 'order', 'icon', 'attributes'));
334
    }
335
336
    /**
337
     * Add new divider item.
338
     *
339
     * @param int   $order
0 ignored issues
show
Documentation introduced by
Should the type for parameter $order not be null|integer?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
340
     * @param array $attributes
341
     *
342
     * @return \Rinvex\Menus\Models\MenuItem
343
     */
344
    public function divider(int $order = null, array $attributes = []): MenuItem
345
    {
346
        $type = 'divider';
347
348
        return $this->add(compact('type', 'order', 'attributes'));
349
    }
350
351
    /**
352
     * Get items count.
353
     *
354
     * @return int
355
     */
356
    public function count(): int
357
    {
358
        return $this->items->count();
359
    }
360
361
    /**
362
     * Empty the current menu items.
363
     *
364
     * @return $this
365
     */
366
    public function destroy()
367
    {
368
        $this->items = collect();
369
370
        return $this;
371
    }
372
373
    /**
374
     * Get menu items and order it by 'order' key.
375
     *
376
     * @return \Illuminate\Support\Collection
377
     */
378
    protected function getOrderedItems(): Collection
379
    {
380
        return $this->items->sortBy('properties.order')->each(function (MenuItem $parent) {
381
            $parent->hideWhen(function () use ($parent) {
382
                return in_array($parent->properties['type'], ['dropdown', 'header']) && ! $parent->getChilds()->reduce(function ($carry, MenuItem $child) {
383
                    return $carry || ! $child->isHidden();
384
                }, false);
385
            });
386
        });
387
    }
388
389
    /**
390
     * Render the menu to HTML tag.
391
     *
392
     * @param string $presenter
0 ignored issues
show
Documentation introduced by
Should the type for parameter $presenter not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
393
     * @param bool   $specialSidebar
394
     *
395
     * @return string
396
     */
397
    public function render(string $presenter = null, bool $specialSidebar = false): string
398
    {
399
        $this->resolveItems($this->items);
400
401
        if (! is_null($this->view)) {
402
            return $this->renderView($presenter, $specialSidebar)->render();
403
        }
404
405
        (! $presenter || ! $this->presenterExists($presenter)) || $this->setPresenter($presenter);
0 ignored issues
show
Bug Best Practice introduced by
The expression $presenter of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
406
407
        return $this->renderMenu($specialSidebar);
408
    }
409
410
    /**
411
     * Render menu via view presenter.
412
     *
413
     * @param string $view
414
     * @param bool   $specialSidebar
415
     *
416
     * @return \Illuminate\Contracts\View\View
417
     */
418
    protected function renderView(string $view, bool $specialSidebar = false): ViewContract
419
    {
420
        return $this->views->make($view, ['items' => $this->getOrderedItems(), 'specialSidebar' => $specialSidebar]);
421
    }
422
423
    /**
424
     * Render the menu.
425
     *
426
     * @param bool $specialSidebar
427
     *
428
     * @return string
429
     */
430
    protected function renderMenu(bool $specialSidebar = false): string
431
    {
432
        $presenter = $this->getPresenter();
433
        $menu = $presenter->getOpenTagWrapper();
434
435
        foreach ($this->getOrderedItems() as $item) {
436
            if ($item->isHidden()) {
437
                continue;
438
            }
439
440
            if ($item->hasChilds()) {
441
                $menu .= $presenter->getMenuWithDropDownWrapper($item, $specialSidebar);
442
            } elseif ($item->isHeader()) {
443
                $menu .= $presenter->getHeaderWrapper($item);
444
            } elseif ($item->isDivider()) {
445
                $menu .= $presenter->getDividerWrapper();
446
            } else {
447
                $menu .= $presenter->getMenuWithoutDropdownWrapper($item);
448
            }
449
        }
450
451
        $menu .= $presenter->getCloseTagWrapper();
452
453
        return $menu;
454
    }
455
456
    /**
457
     * Format URL.
458
     *
459
     * @param string $url
460
     *
461
     * @return string
462
     */
463
    protected function formatUrl(string $url): string
464
    {
465
        $uri = $this->urlPrefix.$url;
466
467
        return $uri === '/' ? '/' : ltrim(rtrim($uri, '/'), '/');
468
    }
469
}
470