Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/ElggMenuBuilder.php (8 issues)

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
Documentation Bug introduced by
$menu_tree is of type array<mixed,mixed|array>, but the property $menu was declared to be of type ElggMenuItem[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

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.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
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 ignore-type  annotation

241
			usort(/** @scrutinizer ignore-type */ $section, $sort_callback);
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 ignore-type  annotation

251
					$node->sortChildren(/** @scrutinizer ignore-type */ $sort_callback);
Loading history...
252 21
					$children = $node->getChildren();
253 21
					if ($children) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type integer which is incompatible with the documented return type boolean.
Loading history...
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
Bug Best Practice introduced by
The expression return $result returns the type integer which is incompatible with the documented return type boolean.
Loading history...
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
Bug Best Practice introduced by
The expression return $a->getData('orig...tData('original_order') returns the type integer which is incompatible with the documented return type boolean.
Loading history...
312
		}
313 3
		return $aw - $bw;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $aw - $bw returns the type integer which is incompatible with the documented return type boolean.
Loading history...
314
	}
315
}
316