Completed
Push — master ( 194bfe...fac0e0 )
by Abdelrahman
01:34
created

MenuGenerator::getPresenter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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 array    $attributes
93
     * @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...
94
     *
95
     * @return \Rinvex\Menus\Models\MenuItem|null
96
     */
97
    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...
98
    {
99
        if (! ($item = $this->findBy('title', $title, $callback))) {
100
            $item = $this->add(compact('title', 'order', 'icon', 'attributes'));
101
            ! is_callable($callback) || call_user_func($callback, $item);
102
        }
103
104
        return $item;
105
    }
106
107
    /**
108
     * Set view factory instance.
109
     *
110
     * @param \Illuminate\View\Factory $views
111
     *
112
     * @return $this
113
     */
114
    public function setViewFactory(ViewFactory $views)
115
    {
116
        $this->views = $views;
117
118
        return $this;
119
    }
120
121
    /**
122
     * Set view.
123
     *
124
     * @param string $view
125
     *
126
     * @return $this
127
     */
128
    public function setView(string $view)
129
    {
130
        $this->view = $view;
131
132
        return $this;
133
    }
134
135
    /**
136
     * Set Prefix URL.
137
     *
138
     * @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...
139
     *
140
     * @return $this
141
     */
142
    public function setUrlPrefix(string $urlPrefix)
143
    {
144
        $this->urlPrefix = $urlPrefix;
145
146
        return $this;
147
    }
148
149
    /**
150
     * Set new presenter class.
151
     *
152
     * @param string $presenter
153
     *
154
     * @return $this
155
     */
156
    public function setPresenter(string $presenter)
157
    {
158
        $this->presenter = app('rinvex.menus.presenters')->get($presenter);
159
160
        return $this;
161
    }
162
163
    /**
164
     * Get presenter instance.
165
     *
166
     * @return \Rinvex\Menus\Contracts\PresenterContract
167
     */
168
    public function getPresenter(): PresenterContract
169
    {
170
        return new $this->presenter();
171
    }
172
173
    /**
174
     * Determine if the given name in the presenter style.
175
     *
176
     * @param string $presenter
177
     *
178
     * @return bool
179
     */
180
    public function presenterExists(string $presenter): bool
181
    {
182
        return app('rinvex.menus.presenters')->has($presenter);
183
    }
184
185
    /**
186
     * Set the resolved item bindings.
187
     *
188
     * @param array $bindings
189
     *
190
     * @return $this
191
     */
192
    public function setBindings(array $bindings)
193
    {
194
        $this->bindings = $bindings;
195
196
        return $this;
197
    }
198
199
    /**
200
     * Resolves a key from the bindings array.
201
     *
202
     * @param string|array $key
203
     *
204
     * @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...
205
     */
206
    public function resolve($key)
207
    {
208
        if (is_array($key)) {
209
            foreach ($key as $k => $v) {
210
                $key[$k] = $this->resolve($v);
211
            }
212
        } elseif (is_string($key)) {
213
            $matches = [];
214
215
            // Search for any {placeholders} and replace with their replacement values
216
            preg_match_all('/{[\s]*?([^\s]+)[\s]*?}/i', $key, $matches, PREG_SET_ORDER);
217
218
            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...
219
                if (array_key_exists($match[1], $this->bindings)) {
220
                    $key = preg_replace('/'.$match[0].'/', $this->bindings[$match[1]], $key, 1);
221
                }
222
            }
223
        }
224
225
        return $key;
226
    }
227
228
    /**
229
     * Resolves an array of menu items properties.
230
     *
231
     * @param \Illuminate\Support\Collection &$items
232
     *
233
     * @return void
234
     */
235
    protected function resolveItems(Collection &$items): void
236
    {
237
        $resolver = function ($property) {
238
            return $this->resolve($property) ?: $property;
239
        };
240
241
        $items->each(function (MenuItem $item) use ($resolver) {
242
            $item->fill(array_map($resolver, $item->properties));
243
        });
244
    }
245
246
    /**
247
     * Add new child menu.
248
     *
249
     * @param array $properties
250
     *
251
     * @return \Rinvex\Menus\Models\MenuItem
252
     */
253
    protected function add(array $properties = []): MenuItem
254
    {
255
        $properties['attributes']['id'] = $properties['attributes']['id'] ?? md5(json_encode($properties));
256
        $this->items->push($item = new MenuItem($properties));
257
258
        return $item;
259
    }
260
261
    /**
262
     * Create new menu with dropdown.
263
     *
264
     * @param callable $callback
265
     * @param string   $title
266
     * @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...
267
     * @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...
268
     * @param array    $attributes
269
     *
270
     * @return \Rinvex\Menus\Models\MenuItem
271
     */
272
    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...
273
    {
274
        call_user_func($callback, $item = $this->add(compact('title', 'order', 'icon', 'attributes')));
275
276
        return $item;
277
    }
278
279
    /**
280
     * Register new menu item using registered route.
281
     *
282
     * @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...
283
     * @param string $title
284
     * @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...
285
     * @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...
286
     * @param array  $attributes
287
     *
288
     * @return \Rinvex\Menus\Models\MenuItem
289
     */
290
    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...
291
    {
292
        return $this->add(compact('route', 'title', 'order', 'icon', 'attributes'));
293
    }
294
295
    /**
296
     * Register new menu item using url.
297
     *
298
     * @param string $url
299
     * @param string $title
300
     * @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...
301
     * @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...
302
     * @param array  $attributes
303
     *
304
     * @return \Rinvex\Menus\Models\MenuItem
305
     */
306
    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...
307
    {
308
        ! $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...
309
310
        return $this->add(compact('url', 'title', 'order', 'icon', 'attributes'));
311
    }
312
313
    /**
314
     * Add new header item.
315
     *
316
     * @param string $title
317
     * @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...
318
     * @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...
319
     * @param array  $attributes
320
     *
321
     * @return \Rinvex\Menus\Models\MenuItem
322
     */
323
    public function header(string $title, int $order = null, string $icon = null, array $attributes = []): MenuItem
324
    {
325
        $type = 'header';
326
327
        return $this->add(compact('type', 'url', 'title', 'order', 'icon', 'attributes'));
328
    }
329
330
    /**
331
     * Add new divider item.
332
     *
333
     * @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...
334
     * @param array $attributes
335
     *
336
     * @return \Rinvex\Menus\Models\MenuItem
337
     */
338
    public function divider(int $order = null, array $attributes = []): MenuItem
339
    {
340
        return $this->add(['type' => 'divider', 'order' => $order, 'attributes' => $attributes]);
341
    }
342
343
    /**
344
     * Get items count.
345
     *
346
     * @return int
347
     */
348
    public function count(): int
349
    {
350
        return $this->items->count();
351
    }
352
353
    /**
354
     * Empty the current menu items.
355
     *
356
     * @return $this
357
     */
358
    public function destroy()
359
    {
360
        $this->items = collect();
361
362
        return $this;
363
    }
364
365
    /**
366
     * Get menu items and order it by 'order' key.
367
     *
368
     * @return \Illuminate\Support\Collection
369
     */
370
    protected function getOrderedItems(): Collection
371
    {
372
        return $this->items->sortBy('properties.order');
373
    }
374
375
    /**
376
     * Render the menu to HTML tag.
377
     *
378
     * @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...
379
     * @param bool   $specialSidebar
380
     *
381
     * @return string
382
     */
383
    public function render(string $presenter = null, bool $specialSidebar = false): string
384
    {
385
        $this->resolveItems($this->items);
386
387
        if (! is_null($this->view)) {
388
            return $this->renderView($presenter, $specialSidebar);
389
        }
390
391
        (! $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...
392
393
        return $this->renderMenu($specialSidebar);
394
    }
395
396
    /**
397
     * Render menu via view presenter.
398
     *
399
     * @param string $view
400
     * @param bool   $specialSidebar
401
     *
402
     * @return \Illuminate\Contracts\View\View
403
     */
404
    protected function renderView(string $view, bool $specialSidebar = false): ViewContract
405
    {
406
        return $this->views->make($view, ['items' => $this->getOrderedItems(), 'specialSidebar' => $specialSidebar]);
407
    }
408
409
    /**
410
     * Render the menu.
411
     *
412
     * @param bool $specialSidebar
413
     *
414
     * @return string
415
     */
416
    protected function renderMenu(bool $specialSidebar = false): string
417
    {
418
        $presenter = $this->getPresenter();
419
        $menu = $presenter->getOpenTagWrapper();
420
421
        foreach ($this->getOrderedItems() as $item) {
422
            if ($item->isHidden()) {
423
                continue;
424
            }
425
426
            if ($item->hasChilds()) {
427
                $menu .= $presenter->getMenuWithDropDownWrapper($item, $specialSidebar);
428
            } elseif ($item->isHeader()) {
429
                $menu .= $presenter->getHeaderWrapper($item);
430
            } elseif ($item->isDivider()) {
431
                $menu .= $presenter->getDividerWrapper();
432
            } else {
433
                $menu .= $presenter->getMenuWithoutDropdownWrapper($item);
434
            }
435
        }
436
437
        $menu .= $presenter->getCloseTagWrapper();
438
439
        return $menu;
440
    }
441
442
    /**
443
     * Format URL.
444
     *
445
     * @param string $url
446
     *
447
     * @return string
448
     */
449
    protected function formatUrl(string $url): string
450
    {
451
        $uri = $this->urlPrefix.$url;
452
453
        return $uri === '/' ? '/' : ltrim(rtrim($uri, '/'), '/');
454
    }
455
}
456