1 | <?php |
||
2 | /** |
||
3 | * Elgg Menu Builder |
||
4 | * |
||
5 | * @package Elgg.Core |
||
6 | * @subpackage Navigation |
||
7 | * @since 1.8.0 |
||
8 | */ |
||
9 | class ElggMenuBuilder { |
||
10 | |||
11 | /** |
||
12 | * @var \ElggMenuItem[] |
||
13 | */ |
||
14 | protected $menu = []; |
||
15 | |||
16 | protected $selected = null; |
||
17 | |||
18 | /** |
||
19 | * \ElggMenuBuilder constructor |
||
20 | * |
||
21 | * @param \ElggMenuItem[] $menu Array of \ElggMenuItem objects |
||
22 | */ |
||
23 | 33 | public function __construct(array $menu) { |
|
24 | 33 | $this->menu = $menu; |
|
25 | 33 | } |
|
26 | |||
27 | /** |
||
28 | * Get a prepared menu array |
||
29 | * |
||
30 | * @param mixed $sort_by Method to sort the menu by. @see \ElggMenuBuilder::sort() |
||
31 | * @return array |
||
32 | */ |
||
33 | 33 | public function getMenu($sort_by = 'text') { |
|
34 | |||
35 | 33 | $this->selectFromContext(); |
|
36 | |||
37 | 33 | $this->selected = $this->findSelected(); |
|
38 | |||
39 | 33 | $this->setupSections(); |
|
40 | |||
41 | 33 | $this->setupTrees(); |
|
42 | |||
43 | 33 | $this->sort($sort_by); |
|
44 | |||
45 | 33 | return $this->menu; |
|
46 | } |
||
47 | |||
48 | /** |
||
49 | * Get the selected menu item |
||
50 | * |
||
51 | * @return \ElggMenuItem |
||
52 | */ |
||
53 | 33 | public function getSelected() { |
|
54 | 33 | return $this->selected; |
|
55 | } |
||
56 | |||
57 | /** |
||
58 | * Select menu items for the current context |
||
59 | * |
||
60 | * @return void |
||
61 | */ |
||
62 | 33 | protected function selectFromContext() { |
|
63 | 33 | if (!isset($this->menu)) { |
|
64 | $this->menu = []; |
||
65 | return; |
||
66 | } |
||
67 | |||
68 | // get menu items for this context |
||
69 | 33 | $selected_menu = []; |
|
70 | 33 | foreach ($this->menu as $menu_item) { |
|
71 | 23 | if (!is_object($menu_item)) { |
|
72 | _elgg_services()->logger->error("A non-object was passed to \ElggMenuBuilder"); |
||
73 | continue; |
||
74 | } |
||
75 | 23 | if ($menu_item->inContext()) { |
|
76 | 23 | $selected_menu[] = $menu_item; |
|
77 | } |
||
78 | } |
||
79 | |||
80 | 33 | $this->menu = $selected_menu; |
|
81 | 33 | } |
|
82 | |||
83 | /** |
||
84 | * Group the menu items into sections |
||
85 | * |
||
86 | * @return void |
||
87 | */ |
||
88 | 33 | protected function setupSections() { |
|
89 | 33 | $sectioned_menu = []; |
|
90 | 33 | foreach ($this->menu as $menu_item) { |
|
91 | 21 | if (!isset($sectioned_menu[$menu_item->getSection()])) { |
|
92 | 21 | $sectioned_menu[$menu_item->getSection()] = []; |
|
93 | } |
||
94 | 21 | $sectioned_menu[$menu_item->getSection()][] = $menu_item; |
|
95 | } |
||
96 | 33 | $this->menu = $sectioned_menu; |
|
97 | 33 | } |
|
98 | |||
99 | /** |
||
100 | * Create trees for each menu section |
||
101 | * |
||
102 | * @internal The tree is doubly linked (parent and children links) |
||
103 | * @return void |
||
104 | */ |
||
105 | 33 | protected function setupTrees() { |
|
106 | 33 | $menu_tree = []; |
|
107 | |||
108 | 33 | foreach ($this->menu as $key => $section) { |
|
109 | 21 | $parents = []; |
|
110 | 21 | $children = []; |
|
111 | 21 | $all_menu_items = []; |
|
112 | |||
113 | // divide base nodes from children |
||
114 | 21 | foreach ($section as $menu_item) { |
|
115 | /* @var \ElggMenuItem $menu_item */ |
||
116 | 21 | $parent_name = $menu_item->getParentName(); |
|
117 | 21 | $menu_item_name = $menu_item->getName(); |
|
118 | |||
119 | 21 | if (!$parent_name) { |
|
120 | // no parents so top level menu items |
||
121 | 21 | $parents[$menu_item_name] = $menu_item; |
|
122 | } else { |
||
123 | 2 | $children[$menu_item_name] = $menu_item; |
|
124 | } |
||
125 | |||
126 | 21 | $all_menu_items[$menu_item_name] = $menu_item; |
|
127 | } |
||
128 | |||
129 | 21 | if (empty($all_menu_items)) { |
|
130 | // empty sections can be skipped |
||
131 | continue; |
||
132 | } |
||
133 | |||
134 | 21 | if (empty($parents)) { |
|
135 | // menu items without parents? That is sad.. report to the log |
||
136 | $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:NoParents'); |
||
137 | _elgg_services()->logger->notice($message); |
||
138 | |||
139 | // skip section as without parents menu can not be drawn |
||
140 | continue; |
||
141 | } |
||
142 | |||
143 | 21 | foreach ($children as $menu_item_name => $menu_item) { |
|
144 | 2 | $parent_name = $menu_item->getParentName(); |
|
145 | |||
146 | 2 | if (!array_key_exists($parent_name, $all_menu_items)) { |
|
147 | // orphaned child, inform authorities and skip to next item |
||
148 | 2 | $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:OrphanedChild', [$menu_item_name, $parent_name]); |
|
149 | 2 | _elgg_services()->logger->notice($message); |
|
150 | |||
151 | 2 | continue; |
|
152 | } |
||
153 | |||
154 | if (!in_array($menu_item, $all_menu_items[$parent_name]->getData('children'))) { |
||
155 | $all_menu_items[$parent_name]->addChild($menu_item); |
||
156 | $menu_item->setParent($all_menu_items[$parent_name]); |
||
157 | } else { |
||
158 | // menu item already existed in parents children, report the duplicate registration |
||
159 | $message = _elgg_services()->translator->translate('ElggMenuBuilder:Trees:DuplicateChild', [$menu_item_name]); |
||
160 | _elgg_services()->logger->notice($message); |
||
161 | |||
162 | continue; |
||
163 | } |
||
164 | } |
||
165 | |||
166 | // convert keys to indexes for first level of tree |
||
167 | 21 | $parents = array_values($parents); |
|
168 | |||
169 | 21 | $menu_tree[$key] = $parents; |
|
170 | } |
||
171 | |||
172 | 33 | $this->menu = $menu_tree; |
|
173 | 33 | } |
|
174 | |||
175 | /** |
||
176 | * Find the menu item that is currently selected |
||
177 | * |
||
178 | * @return \ElggMenuItem |
||
179 | */ |
||
180 | 33 | protected function findSelected() { |
|
181 | |||
182 | // do we have a selected menu item already |
||
183 | 33 | foreach ($this->menu as $menu_item) { |
|
184 | 21 | if ($menu_item->getSelected()) { |
|
185 | 21 | return $menu_item; |
|
186 | } |
||
187 | } |
||
188 | |||
189 | // scan looking for a selected item |
||
190 | 33 | foreach ($this->menu as $menu_item) { |
|
191 | 21 | if ($menu_item->getHref()) { |
|
192 | 13 | if (elgg_http_url_is_identical(current_page_url(), $menu_item->getHref())) { |
|
193 | $menu_item->setSelected(true); |
||
194 | 21 | return $menu_item; |
|
195 | } |
||
196 | } |
||
197 | } |
||
198 | |||
199 | 33 | return null; |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * Sort the menu sections and trees |
||
204 | * |
||
205 | * @param mixed $sort_by Sort type as string or php callback |
||
206 | * @return void |
||
207 | */ |
||
208 | 33 | protected function sort($sort_by) { |
|
209 | |||
210 | // sort sections |
||
211 | 33 | ksort($this->menu); |
|
212 | |||
213 | 33 | switch ($sort_by) { |
|
214 | case 'text': |
||
215 | 17 | $sort_callback = [\ElggMenuBuilder::class, 'compareByText']; |
|
216 | 17 | break; |
|
217 | case 'name': |
||
218 | 2 | $sort_callback = [\ElggMenuBuilder::class, 'compareByName']; |
|
219 | 2 | break; |
|
220 | case 'priority': |
||
221 | 16 | $sort_callback = [\ElggMenuBuilder::class, 'compareByPriority']; |
|
222 | 16 | break; |
|
223 | case 'register': |
||
224 | // use registration order - usort breaks this |
||
225 | return; |
||
226 | break; |
||
227 | default: |
||
228 | if (is_callable($sort_by)) { |
||
229 | $sort_callback = $sort_by; |
||
230 | } else { |
||
231 | return; |
||
232 | } |
||
233 | break; |
||
234 | } |
||
235 | |||
236 | // sort each section |
||
237 | 33 | foreach ($this->menu as $index => $section) { |
|
238 | 21 | foreach ($section as $key => $node) { |
|
239 | 21 | $section[$key]->setData('original_order', $key); |
|
240 | } |
||
241 | 21 | usort($section, $sort_callback); |
|
242 | 21 | $this->menu[$index] = $section; |
|
243 | |||
244 | // depth first traversal of tree |
||
245 | 21 | foreach ($section as $root) { |
|
246 | 21 | $stack = []; |
|
247 | 21 | array_push($stack, $root); |
|
248 | 21 | while (!empty($stack)) { |
|
249 | 21 | $node = array_pop($stack); |
|
250 | /* @var \ElggMenuItem $node */ |
||
251 | 21 | $node->sortChildren($sort_callback); |
|
252 | 21 | $children = $node->getChildren(); |
|
253 | 21 | if ($children) { |
|
0 ignored issues
–
show
|
|||
254 | $stack = array_merge($stack, $children); |
||
255 | } |
||
256 | } |
||
257 | } |
||
258 | } |
||
259 | 33 | } |
|
260 | |||
261 | /** |
||
262 | * Compare two menu items by their display text |
||
263 | * HTML tags are stripped before comparison |
||
264 | * |
||
265 | * @param \ElggMenuItem $a Menu item |
||
266 | * @param \ElggMenuItem $b Menu item |
||
267 | * @return bool |
||
268 | */ |
||
269 | 6 | public static function compareByText($a, $b) { |
|
270 | 6 | $at = strip_tags($a->getText()); |
|
271 | 6 | $bt = strip_tags($b->getText()); |
|
272 | |||
273 | 6 | $result = strnatcmp($at, $bt); |
|
274 | 6 | if ($result === 0) { |
|
275 | return $a->getData('original_order') - $b->getData('original_order'); |
||
276 | } |
||
277 | 6 | return $result; |
|
0 ignored issues
–
show
|
|||
278 | } |
||
279 | |||
280 | /** |
||
281 | * Compare two menu items by their identifiers |
||
282 | * |
||
283 | * @param \ElggMenuItem $a Menu item |
||
284 | * @param \ElggMenuItem $b Menu item |
||
285 | * @return bool |
||
286 | */ |
||
287 | public static function compareByName($a, $b) { |
||
288 | $an = $a->getName(); |
||
289 | $bn = $b->getName(); |
||
290 | |||
291 | $result = strnatcmp($an, $bn); |
||
292 | if ($result === 0) { |
||
293 | return $a->getData('original_order') - $b->getData('original_order'); |
||
294 | } |
||
295 | return $result; |
||
0 ignored issues
–
show
|
|||
296 | } |
||
297 | |||
298 | /** |
||
299 | * Compare two menu items by their priority |
||
300 | * |
||
301 | * @param \ElggMenuItem $a Menu item |
||
302 | * @param \ElggMenuItem $b Menu item |
||
303 | * @return bool |
||
304 | * @since 1.9.0 |
||
305 | */ |
||
306 | 5 | public static function compareByPriority($a, $b) { |
|
307 | 5 | $aw = $a->getPriority(); |
|
308 | 5 | $bw = $b->getPriority(); |
|
309 | |||
310 | 5 | if ($aw == $bw) { |
|
311 | 4 | return $a->getData('original_order') - $b->getData('original_order'); |
|
0 ignored issues
–
show
|
|||
312 | } |
||
313 | 3 | return $aw - $bw; |
|
0 ignored issues
–
show
|
|||
314 | } |
||
315 | } |
||
316 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.