Builder::applyFilters()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 30
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 7
nop 1
dl 0
loc 30
ccs 10
cts 10
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace JeroenNoten\LaravelAdminLte\Menu;
4
5
use Illuminate\Support\Arr;
6
use JeroenNoten\LaravelAdminLte\Helpers\MenuItemHelper;
7
8
/**
9
 * Class Builder.
10
 * Responsible of building and compiling the menu.
11
 *
12
 * @property array $menu
13
 */
14
class Builder
15
{
16
    /**
17
     * A set of constants that will be used to identify where to insert new
18
     * items regarding a particular other item (identified by a key).
19
     */
20
    protected const ADD_AFTER = 0;
21
    protected const ADD_BEFORE = 1;
22
    protected const ADD_INSIDE = 2;
23
24
    /**
25
     * Holds the raw (uncompiled) version of the menu. The menu is a tree-like
26
     * structure where a submenu item plays the role of a node with children.
27
     * All dynamic changes on the menu will be applied over this structure.
28
     *
29
     * @var array
30
     */
31
    protected $rawMenu = [];
32
33
    /**
34
     * Holds the compiled version of the menu, that results of applying all the
35
     * filters to the raw menu items.
36
     *
37
     * @var array
38
     */
39
    protected $compiledMenu = [];
40
41
    /**
42
     * Tells whether the compiled version of the menu should be compiled again
43
     * from the raw version. The idea is to only compile the menu when a client
44
     * is retrieving it and the raw version differs from the compiled version.
45
     *
46
     * @var bool
47
     */
48
    protected $shouldCompile;
49
50
    /**
51
     * Holds the set of filters that will be applied to the menu items. These
52
     * filters will be used in the menu compilation process.
53
     *
54
     * @var array
55
     */
56
    protected $filters;
57
58
    /**
59
     * Constructor.
60
     *
61
     * @param  array  $filters
62
     */
63 66
    public function __construct(array $filters = [])
64
    {
65 66
        $this->filters = $filters;
66 66
        $this->shouldCompile = false;
67
    }
68
69
    /**
70
     * A magic method that allows retrieving properties of the objects generated
71
     * from this class dynamically. We will mainly use this for backward
72
     * compatibility, note the menu was previously accessed through the 'menu'
73
     * property.
74
     *
75
     * @param  string  $key  The name of the property to retrieve
76
     * @return mixed
77
     */
78 61
    public function __get($key)
79
    {
80 61
        return $key === 'menu' ? $this->menu() : null;
81
    }
82
83
    /**
84
     * Gets the compiled version of the menu.
85
     *
86
     * @return array
87
     */
88 61
    public function menu()
89
    {
90
        // First, check if we need to compile the menu again or we can use the
91
        // already compiled version.
92
93 61
        if (! $this->shouldCompile) {
94 35
            return $this->compiledMenu;
95
        }
96
97 61
        $this->compiledMenu = $this->compileItems($this->rawMenu);
98 61
        $this->shouldCompile = false;
99
100 61
        return $this->compiledMenu;
101
    }
102
103
    /**
104
     * Adds new items at the end of the menu.
105
     *
106
     * @param  mixed  $items  The new items to be added
107
     */
108 66
    public function add(...$items)
109
    {
110 66
        array_push($this->rawMenu, ...$items);
111 66
        $this->shouldCompile = true;
112
    }
113
114
    /**
115
     * Adds new items after the specified target menu item.
116
     *
117
     * @param  string  $itemKey  The key that identifies the target menu item
118
     * @param  mixed  $items  The new items to be added
119
     */
120 5
    public function addAfter($itemKey, ...$items)
121
    {
122 5
        $this->addItems($itemKey, self::ADD_AFTER, ...$items);
123
    }
124
125
    /**
126
     * Adds new items before the specified target menu item.
127
     *
128
     * @param  string  $itemKey  The key that identifies the target menu item
129
     * @param  mixed  $items  The new items to be added
130
     */
131 5
    public function addBefore($itemKey, ...$items)
132
    {
133 5
        $this->addItems($itemKey, self::ADD_BEFORE, ...$items);
134
    }
135
136
    /**
137
     * Adds new items inside the specified target menu item. This may be used
138
     * to create or extend a submenu.
139
     *
140
     * @param  string  $itemKey  The key that identifies the target menu item
141
     * @param  mixed  $items  The new items to be added
142
     */
143 5
    public function addIn($itemKey, ...$items)
144
    {
145 5
        $this->addItems($itemKey, self::ADD_INSIDE, ...$items);
146
    }
147
148
    /**
149
     * Removes the specified menu item.
150
     *
151
     * @param  string  $itemKey  The key that identifies the item to remove
152
     */
153 7
    public function remove($itemKey)
154
    {
155
        // Check if a path can be found for the specified menu item.
156
157 7
        if (empty($itemPath = $this->findItemPath($itemKey, $this->rawMenu))) {
158 1
            return;
159
        }
160
161
        // Remove the item from the raw menu.
162
163 6
        Arr::forget($this->rawMenu, implode('.', $itemPath));
164 6
        $this->shouldCompile = true;
165
    }
166
167
    /**
168
     * Checks if exists a menu item with the specified key.
169
     *
170
     * @param  string  $itemKey  The key of the menu item to check for
171
     * @return bool
172
     */
173 3
    public function itemKeyExists($itemKey)
174
    {
175 3
        return ! empty($this->findItemPath($itemKey, $this->rawMenu));
176
    }
177
178
    /**
179
     * Compiles the specified items by applying the filters. Returns an array
180
     * with the compiled items.
181
     *
182
     * @param  array  $items  An array with the items to be compiled
183
     * @return array
184
     */
185 61
    protected function compileItems($items)
186
    {
187
        // Get the set of compiled items.
188
189 61
        $items = array_filter(
190 61
            array_map([$this, 'applyFilters'], $items),
191 61
            [MenuItemHelper::class, 'isAllowed']
192 61
        );
193
194
        // Return the set of compiled items without array holes, that's why we
195
        // use the array_values() method.
196
197 61
        return array_values($items);
198
    }
199
200
    /**
201
     * Finds the path (an array with a sequence of access keys) to the menu item
202
     * specified by its key inside the provided array of elements. A null value
203
     * will be returned if the menu item can't be found.
204
     *
205
     * @param  string  $itemKey  The key of the menu item to find
206
     * @param  array  $items  The array from where to search for the menu item
207
     * @return ?array
208
     */
209 25
    protected function findItemPath($itemKey, $items)
210
    {
211
        // Traverse all the specified items. For each item, we first check if
212
        // the item has the specified key. Otherwise, if the item is a submenu,
213
        // we recursively search for the key and path inside that submenu.
214
215 25
        foreach ($items as $key => $item) {
216 25
            if (isset($item['key']) && $item['key'] === $itemKey) {
217 21
                return [$key];
218 13
            } elseif (MenuItemHelper::isSubmenu($item)) {
219 8
                $subPath = $this->findItemPath($itemKey, $item['submenu']);
220
221 8
                if (! empty($subPath)) {
222 8
                    return array_merge([$key, 'submenu'], $subPath);
223
                }
224
            }
225
        }
226
227
        // Return null when the item is not found.
228
229 6
        return null;
230
    }
231
232
    /**
233
     * Applies all the available filters to a menu item and return the compiled
234
     * version of the item.
235
     *
236
     * @param  mixed  $item  A menu item
237
     * @return mixed
238
     */
239 61
    protected function applyFilters($item)
240
    {
241
        // Filters are only applied to array type menu items.
242
243 61
        if (! is_array($item)) {
244 1
            return $item;
245
        }
246
247
        // If the item is a submenu, compile all the child items first (i.e we
248
        // use a depth-first tree traversal). Note child items needs to be
249
        // compiled first because some of the filters (like the ActiveFilter)
250
        // depends on the children properties when applied on a submenu item.
251
252 61
        if (MenuItemHelper::isSubmenu($item)) {
253 23
            $item['submenu'] = $this->compileItems($item['submenu']);
254
        }
255
256
        // Now, apply all the filters on the root item. Note there is no need
257
        // to continue applying the filters if we detect that the item is not
258
        // allowed to be shown.
259
260 61
        foreach ($this->filters as $filter) {
261 56
            if (! MenuItemHelper::isAllowed($item)) {
262 7
                return $item;
263
            }
264
265 56
            $item = $filter->transform($item);
266
        }
267
268 59
        return $item;
269
    }
270
271
    /**
272
     * Adds new items to the menu in a particular place, relative to a target
273
     * menu item identified by its key.
274
     *
275
     * @param  string  $itemKey  The key that identifies the target menu item
276
     * @param  int  $where  Identifier for where to place the new items
277
     * @param  mixed  $items  The new items to be added
278
     */
279 15
    protected function addItems($itemKey, $where, ...$items)
280
    {
281
        // Check if a path can be found for the specified menu item.
282
283 15
        if (empty($itemPath = $this->findItemPath($itemKey, $this->rawMenu))) {
284 3
            return;
285
        }
286
287
        // Get the index of the specified menu item relative to its parent.
288
289 12
        $itemKeyIdx = end($itemPath);
290 12
        reset($itemPath);
291
292
        // Get the target array where the items should be added, and insert the
293
        // new items there.
294
295 12
        if ($where === self::ADD_INSIDE) {
296 4
            $targetPath = implode('.', array_merge($itemPath, ['submenu']));
297 4
            $targetArr = Arr::get($this->rawMenu, $targetPath, []);
298 4
            array_push($targetArr, ...$items);
299
        } else {
300 8
            $targetPath = implode('.', array_slice($itemPath, 0, -1)) ?: null;
301 8
            $targetArr = Arr::get($this->rawMenu, $targetPath, $this->rawMenu);
302 8
            $offset = ($where === self::ADD_AFTER) ? 1 : 0;
303 8
            array_splice($targetArr, $itemKeyIdx + $offset, 0, $items);
304
        }
305
306
        // Apply changes on the raw menu.
307
308 12
        Arr::set($this->rawMenu, $targetPath, $targetArr);
309 12
        $this->shouldCompile = true;
310
    }
311
}
312