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

ElggMenuBuilder   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 305
Duplicated Lines 0 %

Test Coverage

Coverage 74.81%

Importance

Changes 0
Metric Value
dl 0
loc 305
ccs 98
cts 131
cp 0.7481
rs 8.3157
c 0
b 0
f 0
wmc 43

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getMenu() 0 13 1
A compareByText() 0 9 2
B findSelected() 0 20 6
A setupSections() 0 9 3
A getSelected() 0 2 1
A compareByName() 0 9 2
A __construct() 0 2 1
B selectFromContext() 0 19 5
A compareByPriority() 0 8 2
C sort() 0 47 11
C setupTrees() 0 68 9

How to fix   Complexity   

Complex Class

Complex classes like ElggMenuBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ElggMenuBuilder, and based on these observations, apply Extract Interface, too.

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 Cash Costello
$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;
0 ignored issues
show
Unused Code introduced by Cash Costello
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
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
Bug introduced by Cash Costello
$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
Bug introduced by Cash Costello
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 Cash Costello
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');
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The expression return $a->getData('orig...tData('original_order') returns the type integer which is incompatible with the documented return type boolean.
Loading history...
276
		}
277 6
		return $result;
0 ignored issues
show
Bug Best Practice introduced by Paweł Sroka
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');
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The expression return $a->getData('orig...tData('original_order') returns the type integer which is incompatible with the documented return type boolean.
Loading history...
294
		}
295
		return $result;
0 ignored issues
show
Bug Best Practice introduced by Paweł Sroka
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 Cash Costello
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 Cash Costello
The expression return $aw - $bw returns the type integer which is incompatible with the documented return type boolean.
Loading history...
314
	}
315
}
316