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; |
|||
0 ignored issues
–
show
|
|||||
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); |
|||
0 ignored issues
–
show
$section of type ElggMenuItem is incompatible with the type array expected by parameter $array of usort() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
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); |
|||
0 ignored issues
–
show
It seems like
$sort_callback can also be of type array<integer,string> ; however, parameter $sortFunction of ElggMenuItem::sortChildren() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
252 | 21 | $children = $node->getChildren(); |
|||
253 | 21 | if ($children) { |
|||
0 ignored issues
–
show
The expression
$children of type ElggMenuItem[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
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
Loading history...
|
|||||
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; |
|||
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; |
||||
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'); |
|||
312 | } |
||||
313 | 3 | return $aw - $bw; |
|||
314 | } |
||||
315 | } |
||||
316 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.
Either this assignment is in error or an instanceof check should be added for that assignment.