Completed
Push — develop ( 54607a...e25e8d )
by Abdelrahman
01:24
created

MenuFactory::findByTitleOrAdd()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Menus\Factories;
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
13
class MenuFactory implements Countable
14
{
15
    /**
16
     * The items collection.
17
     *
18
     * @var \Illuminate\Support\Collection
19
     */
20
    protected $items;
21
22
    /**
23
     * The presenter class.
24
     *
25
     * @var string
26
     */
27
    protected $presenter = NavbarPresenter::class;
28
29
    /**
30
     * The URL prefix.
31
     *
32
     * @var string|null
33
     */
34
    protected $urlPrefix;
35
36
    /**
37
     * The view name.
38
     *
39
     * @var string
40
     */
41
    protected $view;
42
43
    /**
44
     * The laravel view factory instance.
45
     *
46
     * @var \Illuminate\View\Factory
47
     */
48
    protected $views;
49
50
    /**
51
     * Resolved item binding map.
52
     *
53
     * @var array
54
     */
55
    protected $bindings = [];
56
57
    /**
58
     * Create a new MenuFactory instance.
59
     */
60
    public function __construct()
61
    {
62
        $this->items = collect();
63
    }
64
65
    /**
66
     * Find menu item by given key and value.
67
     *
68
     * @param string   $key
69
     * @param string   $value
70
     * @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...
71
     *
72
     * @return \Rinvex\Menus\Models\MenuItem
73
     */
74
    public function findBy(string $key, string $value, callable $callback = null)
75
    {
76
        $item = $this->items->filter(function ($item) use ($key, $value) {
77
            return $item->{$key} === $value;
78
        })->first();
79
80
        (! is_callable($callback) || ! $item) || call_user_func($callback, $item);
81
82
        return $item;
83
    }
84
85
    /**
86
     * Find menu item by given key and value.
87
     *
88
     * @param string   $title
89
     * @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...
90
     * @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...
91
     * @param array    $attributes
92
     * @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...
93
     *
94
     * @return \Rinvex\Menus\Models\MenuItem
95
     */
96
    public function findByTitleOrAdd(string $title, int $order = null, string $icon = null, array $attributes = [], callable $callback = null)
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 142 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...
97
    {
98
        if (! ($item = $this->findBy('title', $title, $callback))) {
99
            $item = $this->add(compact('title', 'order', 'icon', 'attributes'));
100
            ! is_callable($callback) || call_user_func($callback, $item);
101
        }
102
103
        return $item;
104
    }
105
106
    /**
107
     * Set view factory instance.
108
     *
109
     * @param \Illuminate\View\Factory $views
110
     *
111
     * @return $this
112
     */
113
    public function setViewFactory(ViewFactory $views)
114
    {
115
        $this->views = $views;
116
117
        return $this;
118
    }
119
120
    /**
121
     * Set view.
122
     *
123
     * @param string $view
124
     *
125
     * @return $this
126
     */
127
    public function setView(string $view)
128
    {
129
        $this->view = $view;
130
131
        return $this;
132
    }
133
134
    /**
135
     * Set Prefix URL.
136
     *
137
     * @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...
138
     *
139
     * @return $this
140
     */
141
    public function setUrlPrefix(string $urlPrefix)
142
    {
143
        $this->urlPrefix = $urlPrefix;
144
145
        return $this;
146
    }
147
148
    /**
149
     * Set new presenter class.
150
     *
151
     * @param string $presenter
152
     *
153
     * @return $this
154
     */
155
    public function setPresenter(string $presenter)
156
    {
157
        $this->presenter = app('rinvex.menus.presenters')->get($presenter);
158
159
        return $this;
160
    }
161
162
    /**
163
     * Get presenter instance.
164
     *
165
     * @return \Rinvex\Menus\Contracts\PresenterContract
166
     */
167
    public function getPresenter()
168
    {
169
        return new $this->presenter();
170
    }
171
172
    /**
173
     * Determine if the given name in the presenter style.
174
     *
175
     * @param string $presenter
176
     *
177
     * @return bool
178
     */
179
    public function presenterExists(string $presenter)
180
    {
181
        return app('rinvex.menus.presenters')->has($presenter);
182
    }
183
184
    /**
185
     * Set the resolved item bindings.
186
     *
187
     * @param array $bindings
188
     *
189
     * @return $this
190
     */
191
    public function setBindings(array $bindings)
192
    {
193
        $this->bindings = $bindings;
194
195
        return $this;
196
    }
197
198
    /**
199
     * Resolves a key from the bindings array.
200
     *
201
     * @param string|array $key
202
     *
203
     * @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...
204
     */
205
    public function resolve($key)
206
    {
207
        if (is_array($key)) {
208
            foreach ($key as $k => $v) {
209
                $key[$k] = $this->resolve($v);
210
            }
211
        } elseif (is_string($key)) {
212
            $matches = [];
213
214
            // Search for any {placeholders} and replace with their replacement values
215
            preg_match_all('/{[\s]*?([^\s]+)[\s]*?}/i', $key, $matches, PREG_SET_ORDER);
216
217
            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...
218
                if (array_key_exists($match[1], $this->bindings)) {
219
                    $key = preg_replace('/'.$match[0].'/', $this->bindings[$match[1]], $key, 1);
220
                }
221
            }
222
        }
223
224
        return $key;
225
    }
226
227
    /**
228
     * Resolves an array of menu items properties.
229
     *
230
     * @param \Illuminate\Support\Collection &$items
231
     *
232
     * @return void
233
     */
234
    protected function resolveItems(Collection &$items)
235
    {
236
        $resolver = function ($property) {
237
            return $this->resolve($property) ?: $property;
238
        };
239
240
        $items->each(function (MenuItem $item) use ($resolver) {
241
            $item->fill(array_map($resolver, $item->properties));
242
        });
243
    }
244
245
    /**
246
     * Add new child menu.
247
     *
248
     * @param array $properties
249
     *
250
     * @return \Rinvex\Menus\Models\MenuItem
251
     */
252
    protected function add(array $properties = [])
253
    {
254
        $properties['attributes']['id'] = $properties['attributes']['id'] ?? md5(json_encode($properties));
255
        $this->items->push($item = new MenuItem($properties));
256
257
        return $item;
258
    }
259
260
    /**
261
     * Create new menu with dropdown.
262
     *
263
     * @param callable $callback
264
     * @param string   $title
265
     * @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...
266
     * @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...
267
     * @param array    $attributes
268
     *
269
     * @return \Rinvex\Menus\Models\MenuItem
270
     */
271
    public function dropdown(callable $callback, string $title, int $order = null, string $icon = null, array $attributes = [])
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 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...
272
    {
273
        call_user_func($callback, $item = $this->add(compact('title', 'order', 'icon', 'attributes')));
274
275
        return $item;
276
    }
277
278
    /**
279
     * Register new menu item using registered route.
280
     *
281
     * @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...
282
     * @param string $title
283
     * @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...
284
     * @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...
285
     * @param array  $attributes
286
     *
287
     * @return \Rinvex\Menus\Models\MenuItem
288
     */
289
    public function route(array $route, string $title, int $order = null, string $icon = null, array $attributes = [])
290
    {
291
        return $this->add(compact('route', 'title', 'order', 'icon', 'attributes'));
292
    }
293
294
    /**
295
     * Register new menu item using url.
296
     *
297
     * @param string $url
298
     * @param string $title
299
     * @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...
300
     * @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...
301
     * @param array  $attributes
302
     *
303
     * @return \Rinvex\Menus\Models\MenuItem
304
     */
305
    public function url(string $url, string $title, int $order = null, string $icon = null, array $attributes = [])
306
    {
307
        ! $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...
308
309
        return $this->add(compact('url', 'title', 'order', 'icon', 'attributes'));
310
    }
311
312
    /**
313
     * Add new header item.
314
     *
315
     * @param string $title
316
     * @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...
317
     * @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...
318
     * @param array  $attributes
319
     *
320
     * @return \Rinvex\Menus\Models\MenuItem
321
     */
322
    public function header(string $title, int $order = null, string $icon = null, array $attributes = [])
323
    {
324
        $type = 'header';
325
326
        return $this->add(compact('type', 'url', 'title', 'order', 'icon', 'attributes'));
327
    }
328
329
    /**
330
     * Add new divider item.
331
     *
332
     * @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...
333
     * @param array $attributes
334
     *
335
     * @return \Rinvex\Menus\Models\MenuItem
336
     */
337
    public function divider(int $order = null, array $attributes = [])
338
    {
339
        return $this->add(['type' => 'divider', 'order' => $order, 'attributes' => $attributes]);
340
    }
341
342
    /**
343
     * Get items count.
344
     *
345
     * @return int
346
     */
347
    public function count()
348
    {
349
        return $this->items->count();
350
    }
351
352
    /**
353
     * Empty the current menu items.
354
     *
355
     * @return $this
356
     */
357
    public function destroy()
358
    {
359
        $this->items = collect();
360
361
        return $this;
362
    }
363
364
    /**
365
     * Get menu items and order it by 'order' key.
366
     *
367
     * @return \Illuminate\Support\Collection
368
     */
369
    protected function getOrderedItems()
370
    {
371
        return $this->items->sortBy('properties.order');
372
    }
373
374
    /**
375
     * Render the menu to HTML tag.
376
     *
377
     * @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...
378
     * @param bool   $specialSidebar
379
     *
380
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be \Illuminate\Contracts\View\View|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
381
     */
382
    public function render(string $presenter = null, bool $specialSidebar = false)
383
    {
384
        $this->resolveItems($this->items);
385
386
        if (! is_null($this->view)) {
387
            return $this->renderView($presenter, $specialSidebar);
388
        }
389
390
        (! $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...
391
392
        return $this->renderMenu($specialSidebar);
393
    }
394
395
    /**
396
     * Render menu via view presenter.
397
     *
398
     * @param string $view
399
     * @param bool   $specialSidebar
400
     *
401
     * @return \Illuminate\Contracts\View\View
402
     */
403
    protected function renderView(string $view, bool $specialSidebar = false)
404
    {
405
        return $this->views->make($view, ['items' => $this->getOrderedItems(), 'specialSidebar' => $specialSidebar]);
406
    }
407
408
    /**
409
     * Render the menu.
410
     *
411
     * @param bool $specialSidebar
412
     *
413
     * @return string
414
     */
415
    protected function renderMenu(bool $specialSidebar = false)
416
    {
417
        $presenter = $this->getPresenter();
418
        $menu = $presenter->getOpenTagWrapper();
419
420
        foreach ($this->getOrderedItems() as $item) {
421
            if ($item->hidden()) {
422
                continue;
423
            }
424
425
            if ($item->hasChilds()) {
426
                $menu .= $presenter->getMenuWithDropDownWrapper($item, $specialSidebar);
427
            } elseif ($item->isHeader()) {
428
                $menu .= $presenter->getHeaderWrapper($item);
429
            } elseif ($item->isDivider()) {
430
                $menu .= $presenter->getDividerWrapper();
431
            } else {
432
                $menu .= $presenter->getMenuWithoutDropdownWrapper($item);
433
            }
434
        }
435
436
        $menu .= $presenter->getCloseTagWrapper();
437
438
        return $menu;
439
    }
440
441
    /**
442
     * Format URL.
443
     *
444
     * @param string $url
445
     *
446
     * @return string
447
     */
448
    protected function formatUrl(string $url)
449
    {
450
        $uri = $this->urlPrefix.$url;
451
452
        return $uri === '/' ? '/' : ltrim(rtrim($uri, '/'), '/');
453
    }
454
}
455