Passed
Pull Request — master (#1277)
by Diego
04:08
created

Builder::transformItems()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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