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) { |
|
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'); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
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'); |
||
0 ignored issues
–
show
|
|||
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 |