|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Spatie\Menu; |
|
4
|
|
|
|
|
5
|
|
|
use ReflectionFunction; |
|
6
|
|
|
use ReflectionParameter; |
|
7
|
|
|
use Spatie\HtmlElement\Html; |
|
8
|
|
|
use Spatie\Menu\Items\Link; |
|
9
|
|
|
use Spatie\Menu\Traits\HtmlAttributes; |
|
10
|
|
|
use Spatie\Menu\Traits\ParentAttributes; |
|
11
|
|
|
|
|
12
|
|
|
class Menu implements Item |
|
13
|
|
|
{ |
|
14
|
|
|
use HtmlAttributes, ParentAttributes; |
|
15
|
|
|
|
|
16
|
|
|
/** @var array */ |
|
17
|
|
|
protected $items = []; |
|
18
|
|
|
|
|
19
|
|
|
/** @var string */ |
|
20
|
|
|
protected $prepend = ''; |
|
21
|
|
|
|
|
22
|
|
|
/** @var string */ |
|
23
|
|
|
protected $append = ''; |
|
24
|
|
|
|
|
25
|
|
|
/** @var array */ |
|
26
|
|
|
protected $filters = []; |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* @param \Spatie\Menu\Item[] ...$items |
|
30
|
|
|
*/ |
|
31
|
|
|
protected function __construct(Item ...$items) |
|
32
|
|
|
{ |
|
33
|
|
|
$this->items = $items; |
|
34
|
|
|
} |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* Create a new menu, optionally prefilled with items. |
|
38
|
|
|
* |
|
39
|
|
|
* @param array $items |
|
40
|
|
|
* |
|
41
|
|
|
* @return static |
|
42
|
|
|
*/ |
|
43
|
|
|
public static function new(array $items = []) |
|
44
|
|
|
{ |
|
45
|
|
|
return new static(...$items); |
|
46
|
|
|
} |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* Add an item to the menu. This also applies all registered filters on the item. If a filter |
|
50
|
|
|
* returns false, the item won't be added. |
|
51
|
|
|
* |
|
52
|
|
|
* @param \Spatie\Menu\Item $item |
|
53
|
|
|
* |
|
54
|
|
|
* @return $this |
|
55
|
|
|
*/ |
|
56
|
|
|
public function add(Item $item) |
|
57
|
|
|
{ |
|
58
|
|
|
if ($this->applyFilters($item) === false) { |
|
59
|
|
|
return $this; |
|
60
|
|
|
} |
|
61
|
|
|
|
|
62
|
|
|
$this->items[] = $item; |
|
63
|
|
|
|
|
64
|
|
|
return $this; |
|
65
|
|
|
} |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* Applies all the currently registered filters to an item. |
|
69
|
|
|
* |
|
70
|
|
|
* @param \Spatie\Menu\Item $item |
|
71
|
|
|
* |
|
72
|
|
|
* @return bool |
|
73
|
|
|
*/ |
|
74
|
|
|
protected function applyFilters(Item $item) : bool |
|
75
|
|
|
{ |
|
76
|
|
|
foreach ($this->filters as $filter) { |
|
77
|
|
|
$type = $this->determineFirstParameterType($filter); |
|
78
|
|
|
|
|
79
|
|
|
if ($type !== null && !$item instanceof $type) { |
|
80
|
|
|
continue; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
if ($filter($item) === false) { |
|
84
|
|
|
return false; |
|
85
|
|
|
} |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
return true; |
|
89
|
|
|
} |
|
90
|
|
|
|
|
91
|
|
|
/** |
|
92
|
|
|
* Map through all the items and return an array containing the result. If you typehint the |
|
93
|
|
|
* item parameter in the callable, it wil only be applied to items of that type. |
|
94
|
|
|
* |
|
95
|
|
|
* @param callable $callable |
|
96
|
|
|
* |
|
97
|
|
|
* @return array |
|
98
|
|
|
*/ |
|
99
|
|
|
public function map(callable $callable) : array |
|
100
|
|
|
{ |
|
101
|
|
|
$type = $this->determineFirstParameterType($callable); |
|
102
|
|
|
|
|
103
|
|
|
$items = $this->items; |
|
104
|
|
|
|
|
105
|
|
|
if ($type !== null) { |
|
106
|
|
|
$items = array_filter($items, function (Item $item) use ($type) { |
|
107
|
|
|
return $item instanceof $type; |
|
108
|
|
|
}); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
return array_map($callable, $items); |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Iterate over all the items and apply a callback. If you typehint the |
|
116
|
|
|
* item parameter in the callable, it wil only be applied to items of that type. |
|
117
|
|
|
* |
|
118
|
|
|
* @param callable $callable |
|
119
|
|
|
* |
|
120
|
|
|
* @return $this |
|
121
|
|
|
*/ |
|
122
|
|
View Code Duplication |
public function each(callable $callable) |
|
|
|
|
|
|
123
|
|
|
{ |
|
124
|
|
|
$type = $this->determineFirstParameterType($callable); |
|
125
|
|
|
|
|
126
|
|
|
foreach ($this->items as $item) { |
|
127
|
|
|
if ($type !== null && !$item instanceof $type) { |
|
128
|
|
|
continue; |
|
129
|
|
|
} |
|
130
|
|
|
|
|
131
|
|
|
$callable($item); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
return $this; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* Register a filter to the menu. When an item is added, all filters will be applied to the |
|
139
|
|
|
* item. If a filter returns false, the item won't be added. If you typehint the item |
|
140
|
|
|
* parameter in the callable, it wil only be applied to items of that type. |
|
141
|
|
|
* |
|
142
|
|
|
* @param callable $callable |
|
143
|
|
|
* |
|
144
|
|
|
* @return $this |
|
145
|
|
|
*/ |
|
146
|
|
|
public function registerFilter(callable $callable) |
|
147
|
|
|
{ |
|
148
|
|
|
$this->filters[] = $callable; |
|
149
|
|
|
|
|
150
|
|
|
return $this; |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Apply a callable to all existing items, and register it as a filter so it will get applied |
|
155
|
|
|
* to all new items too. If you typehint the item parameter in the callable, it wil only be |
|
156
|
|
|
* applied to items of that type. |
|
157
|
|
|
* |
|
158
|
|
|
* @param callable $callable |
|
159
|
|
|
* |
|
160
|
|
|
* @return $this |
|
161
|
|
|
*/ |
|
162
|
|
|
public function applyToAll(callable $callable) |
|
163
|
|
|
{ |
|
164
|
|
|
$this->each($callable); |
|
165
|
|
|
$this->registerFilter($callable); |
|
166
|
|
|
|
|
167
|
|
|
return $this; |
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
/** |
|
171
|
|
|
* Determine the type of the first parameter of a callable. |
|
172
|
|
|
* |
|
173
|
|
|
* @param callable $callable |
|
174
|
|
|
* |
|
175
|
|
|
* @return string|null |
|
176
|
|
|
*/ |
|
177
|
|
|
protected function determineFirstParameterType(callable $callable) |
|
178
|
|
|
{ |
|
179
|
|
|
$reflection = new ReflectionFunction($callable); |
|
180
|
|
|
|
|
181
|
|
|
$parameterTypes = array_map(function (ReflectionParameter $parameter) { |
|
182
|
|
|
return $parameter->getClass() ? $parameter->getClass()->name : null; |
|
183
|
|
|
}, $reflection->getParameters()); |
|
184
|
|
|
|
|
185
|
|
|
return $parameterTypes[0] ?? null; |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
/** |
|
189
|
|
|
* Prepend a string of html to the menu on render. |
|
190
|
|
|
* |
|
191
|
|
|
* @param string $prefix |
|
192
|
|
|
* |
|
193
|
|
|
* @return $this |
|
194
|
|
|
*/ |
|
195
|
|
|
public function prefixLinks(string $prefix) |
|
196
|
|
|
{ |
|
197
|
|
|
return $this->applyToAll(function (Link $link) use ($prefix) { |
|
198
|
|
|
$link->prefix($prefix); |
|
199
|
|
|
}); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* Prepend the menu with a string of html on render. |
|
204
|
|
|
* |
|
205
|
|
|
* @param string $prepend |
|
206
|
|
|
* |
|
207
|
|
|
* @return $this |
|
208
|
|
|
*/ |
|
209
|
|
|
public function prepend(string $prepend) |
|
210
|
|
|
{ |
|
211
|
|
|
$this->prepend = $prepend; |
|
212
|
|
|
|
|
213
|
|
|
return $this; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
/** |
|
217
|
|
|
* Append a string of html to the menu on render. |
|
218
|
|
|
* |
|
219
|
|
|
* @param string $append |
|
220
|
|
|
* |
|
221
|
|
|
* @return $this |
|
222
|
|
|
*/ |
|
223
|
|
|
public function append(string $append) |
|
224
|
|
|
{ |
|
225
|
|
|
$this->append = $append; |
|
226
|
|
|
|
|
227
|
|
|
return $this; |
|
228
|
|
|
} |
|
229
|
|
|
|
|
230
|
|
|
/** |
|
231
|
|
|
* Determine whether the menu is active. |
|
232
|
|
|
* |
|
233
|
|
|
* @return bool |
|
234
|
|
|
*/ |
|
235
|
|
|
public function isActive() : bool |
|
236
|
|
|
{ |
|
237
|
|
|
foreach ($this->items as $item) { |
|
238
|
|
|
if ($item->isActive()) { |
|
239
|
|
|
return true; |
|
240
|
|
|
} |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
return false; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* Set multiple items in the menu as active based on a callable that filters through items. |
|
248
|
|
|
* If you typehint the item parameter in the callable, it wil only be applied to items of |
|
249
|
|
|
* that type. |
|
250
|
|
|
* |
|
251
|
|
|
* @param callable $callable |
|
252
|
|
|
* |
|
253
|
|
|
* @return $this |
|
254
|
|
|
*/ |
|
255
|
|
View Code Duplication |
public function setActive(callable $callable) |
|
|
|
|
|
|
256
|
|
|
{ |
|
257
|
|
|
$type = $this->determineFirstParameterType($callable); |
|
258
|
|
|
|
|
259
|
|
|
foreach ($this->items as $item) { |
|
260
|
|
|
if ($type && !$item instanceof $type) { |
|
|
|
|
|
|
261
|
|
|
continue; |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
if ($callable($item)) { |
|
265
|
|
|
$item->setActive(); |
|
266
|
|
|
} |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
return $this; |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* Render the menu in html. |
|
274
|
|
|
* |
|
275
|
|
|
* @return string |
|
276
|
|
|
*/ |
|
277
|
|
|
public function render() : string |
|
278
|
|
|
{ |
|
279
|
|
|
$menu = Html::el( |
|
280
|
|
|
'ul', |
|
281
|
|
|
$this->attributes()->toArray(), |
|
282
|
|
|
$this->map(function (Item $item) { |
|
283
|
|
|
return Html::el( |
|
284
|
|
|
$item->isActive() ? 'li.active' : 'li', |
|
285
|
|
|
$item->getParentAttributes(), |
|
286
|
|
|
$item->render() |
|
287
|
|
|
); |
|
288
|
|
|
}) |
|
289
|
|
|
); |
|
290
|
|
|
|
|
291
|
|
|
return "{$this->prepend}{$menu}{$this->append}"; |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
public function __toString() : string |
|
295
|
|
|
{ |
|
296
|
|
|
return $this->render(); |
|
297
|
|
|
} |
|
298
|
|
|
} |
|
299
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.