Completed
Push — develop ( fe6449...98c976 )
by Abdelrahman
11:45
created

MenuGenerator::resolveItems()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 10
Code Lines 5

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 5
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Menus\Models;
6
7
use Countable;
8
use Rinvex\Menus\Models\MenuItem;
9
use Illuminate\Support\Collection;
10
use Illuminate\View\Factory as ViewFactory;
11
use Rinvex\Menus\Presenters\NavbarPresenter;
12
use Rinvex\Menus\Contracts\PresenterContract;
13
use Illuminate\Contracts\View\View as ViewContract;
14
15
class MenuGenerator implements Countable
16
{
17
    /**
18
     * The items collection.
19
     *
20
     * @var \Illuminate\Support\Collection
21
     */
22
    protected $items;
23
24
    /**
25
     * The presenter class.
26
     *
27
     * @var string
28
     */
29
    protected $presenter = NavbarPresenter::class;
30
31
    /**
32
     * The URL prefix.
33
     *
34
     * @var string|null
35
     */
36
    protected $urlPrefix;
37
38
    /**
39
     * The view name.
40
     *
41
     * @var string
42
     */
43
    protected $view;
44
45
    /**
46
     * The laravel view factory instance.
47
     *
48
     * @var \Illuminate\View\Factory
49
     */
50
    protected $views;
51
52
    /**
53
     * Resolved item binding map.
54
     *
55
     * @var array
56
     */
57
    protected $bindings = [];
58
59
    /**
60
     * Create a new MenuGenerator instance.
61
     */
62
    public function __construct()
63
    {
64
        $this->items = collect();
65
    }
66
67
    /**
68
     * Find menu item by given key and value.
69
     *
70
     * @param string   $key
71
     * @param string   $value
72
     * @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...
73
     *
74
     * @return \Rinvex\Menus\Models\MenuItem|null
75
     */
76
    public function findBy(string $key, string $value, callable $callback = null): ?MenuItem
77
    {
78
        $item = $this->items->filter(function ($item) use ($key, $value) {
79
            return $item->{$key} === $value;
80
        })->first();
81
82
        (! is_callable($callback) || ! $item) || call_user_func($callback, $item);
83
84
        return $item;
85
    }
86
87
    /**
88
     * Find menu item by given key and value.
89
     *
90
     * @param string   $title
91
     * @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...
92
     * @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...
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, array $attributes = [], callable $callback = null): ?MenuItem
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 153 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
99
    {
100
        if (! ($item = $this->findBy('title', $title, $callback))) {
101
            $item = $this->add(compact('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
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 137 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
274
    {
275
        call_user_func($callback, $item = $this->add(compact('title', 'order', 'icon', 'attributes')));
276
277
        return $item;
278
    }
279
280
    /**
281
     * Register new menu item using registered route.
282
     *
283
     * @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...
284
     * @param string $title
285
     * @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...
286
     * @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...
287
     * @param array  $attributes
288
     *
289
     * @return \Rinvex\Menus\Models\MenuItem
290
     */
291
    public function route(array $route, string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
292
    {
293
        return $this->add(compact('route', 'title', 'order', 'icon', 'attributes'));
294
    }
295
296
    /**
297
     * Register new menu item using url.
298
     *
299
     * @param string $url
300
     * @param string $title
301
     * @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...
302
     * @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...
303
     * @param array  $attributes
304
     *
305
     * @return \Rinvex\Menus\Models\MenuItem
306
     */
307
    public function url(string $url, string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 125 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
308
    {
309
        ! $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...
310
311
        return $this->add(compact('url', 'title', 'order', 'icon', 'attributes'));
312
    }
313
314
    /**
315
     * Add new header item.
316
     *
317
     * @param string $title
318
     * @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...
319
     * @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...
320
     * @param array  $attributes
321
     *
322
     * @return \Rinvex\Menus\Models\MenuItem
323
     */
324
    public function header(string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
325
    {
326
        $type = 'header';
327
328
        return $this->add(compact('type', 'url', 'title', 'order', 'icon', 'attributes'));
329
    }
330
331
    /**
332
     * Add new divider item.
333
     *
334
     * @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...
335
     * @param array $attributes
336
     *
337
     * @return \Rinvex\Menus\Models\MenuItem
338
     */
339
    public function divider(int $order = null, array $attributes = []): MenuItem
340
    {
341
        return $this->add(['type' => 'divider', 'order' => $order, 'attributes' => $attributes]);
342
    }
343
344
    /**
345
     * Get items count.
346
     *
347
     * @return int
348
     */
349
    public function count(): int
350
    {
351
        return $this->items->count();
352
    }
353
354
    /**
355
     * Empty the current menu items.
356
     *
357
     * @return $this
358
     */
359
    public function destroy()
360
    {
361
        $this->items = collect();
362
363
        return $this;
364
    }
365
366
    /**
367
     * Get menu items and order it by 'order' key.
368
     *
369
     * @return \Illuminate\Support\Collection
370
     */
371
    protected function getOrderedItems(): Collection
372
    {
373
        return $this->items->sortBy('properties.order');
374
    }
375
376
    /**
377
     * Render the menu to HTML tag.
378
     *
379
     * @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...
380
     * @param bool   $specialSidebar
381
     *
382
     * @return string
383
     */
384
    public function render(string $presenter = null, bool $specialSidebar = false): string
385
    {
386
        $this->resolveItems($this->items);
387
388
        if (! is_null($this->view)) {
389
            return $this->renderView($presenter, $specialSidebar);
390
        }
391
392
        (! $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...
393
394
        return $this->renderMenu($specialSidebar);
395
    }
396
397
    /**
398
     * Render menu via view presenter.
399
     *
400
     * @param string $view
401
     * @param bool   $specialSidebar
402
     *
403
     * @return \Illuminate\Contracts\View\View
404
     */
405
    protected function renderView(string $view, bool $specialSidebar = false): ViewContract
406
    {
407
        return $this->views->make($view, ['items' => $this->getOrderedItems(), 'specialSidebar' => $specialSidebar]);
408
    }
409
410
    /**
411
     * Render the menu.
412
     *
413
     * @param bool $specialSidebar
414
     *
415
     * @return string
416
     */
417
    protected function renderMenu(bool $specialSidebar = false): string
418
    {
419
        $presenter = $this->getPresenter();
420
        $menu = $presenter->getOpenTagWrapper();
421
422
        foreach ($this->getOrderedItems() as $item) {
423
            if ($item->isHidden()) {
424
                continue;
425
            }
426
427
            if ($item->hasChilds()) {
428
                $menu .= $presenter->getMenuWithDropDownWrapper($item, $specialSidebar);
429
            } elseif ($item->isHeader()) {
430
                $menu .= $presenter->getHeaderWrapper($item);
431
            } elseif ($item->isDivider()) {
432
                $menu .= $presenter->getDividerWrapper();
433
            } else {
434
                $menu .= $presenter->getMenuWithoutDropdownWrapper($item);
435
            }
436
        }
437
438
        $menu .= $presenter->getCloseTagWrapper();
439
440
        return $menu;
441
    }
442
443
    /**
444
     * Format URL.
445
     *
446
     * @param string $url
447
     *
448
     * @return string
449
     */
450
    protected function formatUrl(string $url): string
451
    {
452
        $uri = $this->urlPrefix.$url;
453
454
        return $uri === '/' ? '/' : ltrim(rtrim($uri, '/'), '/');
455
    }
456
}
457