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

ElggPriorityList   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Test Coverage

Coverage 61.63%

Importance

Changes 0
Metric Value
dl 0
loc 270
rs 10
c 0
b 0
f 0
ccs 45
cts 73
cp 0.6163
wmc 30

17 Methods

Rating   Name   Duplication   Size   Complexity  
A next() 0 3 1
A current() 0 3 1
A getElement() 0 2 2
A sortIfUnsorted() 0 3 2
A key() 0 3 1
A add() 0 10 3
A __construct() 0 4 3
A count() 0 2 1
A valid() 0 4 2
A getNextPriority() 0 8 2
A remove() 0 7 2
A getPriority() 0 2 1
A getElements() 0 3 1
A sort() 0 15 3
A contains() 0 2 1
A rewind() 0 3 1
A move() 0 16 3
1
<?php
2
/**
3
 * Iterate over elements in a specific priority.
4
 *
5
 * $pl = new \ElggPriorityList();
6
 * $pl->add('Element 0');
7
 * $pl->add('Element 10', 10);
8
 * $pl->add('Element -10', -10);
9
 *
10
 * foreach ($pl as $priority => $element) {
11
 *	var_dump("$priority => $element");
12
 * }
13
 *
14
 * Yields:
15
 * -10 => Element -10
16
 * 0 => Element 0
17
 * 10 => Element 10
18
 *
19
 * Collisions on priority are handled by inserting the element at or as close to the
20
 * requested priority as possible:
21
 *
22
 * $pl = new \ElggPriorityList();
23
 * $pl->add('Element 5', 5);
24
 * $pl->add('Colliding element 5', 5);
25
 * $pl->add('Another colliding element 5', 5);
26
 *
27
 * foreach ($pl as $priority => $element) {
28
 *	var_dump("$priority => $element");
29
 * }
30
 *
31
 * Yields:
32
 *	5 => 'Element 5',
33
 *	6 => 'Colliding element 5',
34
 *	7 => 'Another colliding element 5'
35
 *
36
 * You can do priority lookups by element:
37
 *
38
 * $pl = new \ElggPriorityList();
39
 * $pl->add('Element 0');
40
 * $pl->add('Element -5', -5);
41
 * $pl->add('Element 10', 10);
42
 * $pl->add('Element -10', -10);
43
 *
44
 * $priority = $pl->getPriority('Element -5');
45
 *
46
 * Or element lookups by priority.
47
 * $element = $pl->getElement(-5);
48
 *
49
 * To remove elements, pass the element.
50
 * $pl->remove('Element -10');
51
 *
52
 * To check if an element exists:
53
 * $pl->contains('Element -5');
54
 *
55
 * To move an element:
56
 * $pl->move('Element -5', -3);
57
 *
58
 * \ElggPriorityList only tracks priority. No checking is done in \ElggPriorityList for duplicates or
59
 * updating. If you need to track this use objects and an external map:
60
 *
61
 * function elgg_register_something($id, $display_name, $location, $priority = 500) {
62
 *	// $id => $element.
63
 *	static $map = array();
64
 *	static $list;
65
 *
66
 *	if (!$list) {
67
 *		$list = new \ElggPriorityList();
68
 *	}
69
 *
70
 *	// update if already registered.
71
 *	if (isset($map[$id])) {
72
 *		$element = $map[$id];
73
 *		// move it first because we have to pass the original element.
74
 *		if (!$list->move($element, $priority)) {
75
 *			return false;
76
 *		}
77
 *		$element->display_name = $display_name;
78
 *		$element->location = $location;
79
 *	} else {
80
 *		$element = new \stdClass();
81
 *		$element->display_name = $display_name;
82
 *		$element->location = $location;
83
 *		if (!$list->add($element, $priority)) {
84
 *			return false;
85
 *		}
86
 *		$map[$id] = $element;
87
 *	}
88
 *
89
 *	return true;
90
 * }
91
 *
92
 * @package    Elgg.Core
93
 * @subpackage Helpers
94
 */
95
class ElggPriorityList
96
	implements \Iterator, \Countable {
97
98
	/**
99
	 * The list of elements
100
	 *
101
	 * @var array
102
	 */
103
	private $elements = [];
104
105
	/**
106
	 * Create a new priority list.
107
	 *
108
	 * @param array $elements An optional array of priorities => element
109
	 */
110 25
	public function __construct(array $elements = []) {
111 25
		if ($elements) {
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The expression $elements of type array 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...
112
			foreach ($elements as $priority => $element) {
113
				$this->add($element, $priority);
114
			}
115
		}
116 25
	}
117
118
	/**
119
	 * Adds an element to the list.
120
	 *
121
	 * @warning This returns the priority at which the element was added, which can be 0. Use
122
	 *          !== false to check for success.
123
	 *
124
	 * @param mixed $element  The element to add to the list.
125
	 * @param mixed $priority Priority to add the element. In priority collisions, the original element
126
	 *                        maintains its priority and the new element is to the next available
127
	 *                        slot, taking into consideration all previously registered elements.
128
	 *                        Negative elements are accepted.
129
	 * @param bool  $exact    unused
130
	 * @return int            The priority of the added element.
131
	 * @todo remove $exact or implement it. Note we use variable name strict below.
132
	 */
133 39
	public function add($element, $priority = null, $exact = false) {
1 ignored issue
show
Unused Code introduced by Brett Profitt
The parameter $exact is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

133
	public function add($element, $priority = null, /** @scrutinizer ignore-unused */ $exact = false) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
134 39
		if ($priority !== null && !is_numeric($priority)) {
135
			return false;
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
136
		} else {
137 39
			$priority = $this->getNextPriority($priority);
138
		}
139
140 39
		$this->elements[$priority] = $element;
141 39
		$this->sorted = false;
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The property sorted does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
142 39
		return $priority;
143
	}
144
145
	/**
146
	 * Removes an element from the list.
147
	 *
148
	 * @warning The element must have the same attributes / values. If using $strict, it must have
149
	 *          the same types. array(10) will fail in strict against array('10') (str vs int).
150
	 *
151
	 * @param mixed $element The element to remove from the list
152
	 * @param bool  $strict  Whether to check the type of the element match
153
	 * @return bool
154
	 */
155 2
	public function remove($element, $strict = false) {
156 2
		$index = array_search($element, $this->elements, $strict);
157 2
		if ($index !== false) {
158 2
			unset($this->elements[$index]);
159 2
			return true;
160
		} else {
161
			return false;
162
		}
163
	}
164
165
	/**
166
	 * Move an existing element to a new priority.
167
	 *
168
	 * @param mixed $element      The element to move
169
	 * @param int   $new_priority The new priority for the element
170
	 * @param bool  $strict       Whether to check the type of the element match
171
	 * @return bool
172
	 */
173 15
	public function move($element, $new_priority, $strict = false) {
174 15
		$new_priority = (int) $new_priority;
175
		
176 15
		$current_priority = $this->getPriority($element, $strict);
177 15
		if ($current_priority === false) {
178
			return false;
179
		}
180
181 15
		if ($current_priority == $new_priority) {
182
			return true;
183
		}
184
185
		// move the actual element so strict operations still work
186 15
		$element = $this->getElement($current_priority);
0 ignored issues
show
Bug introduced by Brett Profitt
It seems like $current_priority can also be of type string; however, parameter $priority of ElggPriorityList::getElement() does only seem to accept integer, 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

186
		$element = $this->getElement(/** @scrutinizer ignore-type */ $current_priority);
Loading history...
187 15
		unset($this->elements[$current_priority]);
188 15
		return $this->add($element, $new_priority);
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The expression return $this->add($element, $new_priority) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
189
	}
190
191
	/**
192
	 * Returns the elements
193
	 *
194
	 * @return array
195
	 */
196 8
	public function getElements() {
197 8
		$this->sortIfUnsorted();
198 8
		return $this->elements;
199
	}
200
201
	/**
202
	 * Sort the elements optionally by a callback function.
203
	 *
204
	 * If no user function is provided the elements are sorted by priority registered.
205
	 *
206
	 * The callback function should accept the array of elements as the first
207
	 * argument and should return a sorted array.
208
	 *
209
	 * This function can be called multiple times.
210
	 *
211
	 * @param callback $callback The callback for sorting. Numeric sorting is the default.
212
	 * @return bool
213
	 */
214 7
	public function sort($callback = null) {
215 7
		if (!$callback) {
216 7
			ksort($this->elements, SORT_NUMERIC);
217
		} else {
218
			$sorted = call_user_func($callback, $this->elements);
219
220
			if (!$sorted) {
221
				return false;
222
			}
223
224
			$this->elements = $sorted;
225
		}
226
		
227 7
		$this->sorted = true;
0 ignored issues
show
Bug Best Practice introduced by Brett Profitt
The property sorted does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
228 7
		return true;
229
	}
230
231
	/**
232
	 * Sort the elements if they haven't been sorted yet.
233
	 *
234
	 * @return bool
235
	 */
236 8
	private function sortIfUnsorted() {
237 8
		if (!$this->sorted) {
238 7
			return $this->sort();
239
		}
240 6
	}
241
242
	/**
243
	 * Returns the next priority available.
244
	 *
245
	 * @param int $near Make the priority as close to $near as possible.
246
	 * @return int
247
	 */
248 39
	public function getNextPriority($near = 0) {
249 39
		$near = (int) $near;
250
		
251 39
		while (array_key_exists($near, $this->elements)) {
252 33
			$near++;
253
		}
254
255 39
		return $near;
256
	}
257
258
	/**
259
	 * Returns the priority of an element if it exists in the list.
260
	 *
261
	 * @warning This can return 0 if the element's priority is 0.
262
	 *
263
	 * @param mixed $element The element to check for.
264
	 * @param bool  $strict  Use strict checking?
265
	 * @return mixed False if the element doesn't exists, the priority if it does.
266
	 */
267 18
	public function getPriority($element, $strict = false) {
268 18
		return array_search($element, $this->elements, $strict);
269
	}
270
271
	/**
272
	 * Returns the element at $priority.
273
	 *
274
	 * @param int $priority The priority
275
	 * @return mixed The element or false on fail.
276
	 */
277 15
	public function getElement($priority) {
278 15
		return (isset($this->elements[$priority])) ? $this->elements[$priority] : false;
279
	}
280
281
	/**
282
	 * Returns if the list contains $element.
283
	 *
284
	 * @param mixed $element The element to check.
285
	 * @param bool  $strict  Use strict checking?
286
	 * @return bool
287
	 */
288 15
	public function contains($element, $strict = false) {
289 15
		return $this->getPriority($element, $strict) !== false;
290
	}
291
292
	
293
	/**********************
294
	 * Interface methods *
295
	 **********************/
296
297
	/**
298
	 * Iterator
299
	 */
300
301
	/**
302
	 * PHP Iterator Interface
303
	 *
304
	 * @see Iterator::rewind()
305
	 * @return void
306
	 */
307
	public function rewind() {
308
		$this->sortIfUnsorted();
309
		return reset($this->elements);
310
	}
311
312
	/**
313
	 * PHP Iterator Interface
314
	 *
315
	 * @see Iterator::current()
316
	 * @return mixed
317
	 */
318
	public function current() {
319
		$this->sortIfUnsorted();
320
		return current($this->elements);
321
	}
322
323
	/**
324
	 * PHP Iterator Interface
325
	 *
326
	 * @see Iterator::key()
327
	 * @return int
328
	 */
329
	public function key() {
330
		$this->sortIfUnsorted();
331
		return key($this->elements);
1 ignored issue
show
Bug Best Practice introduced by Brett Profitt
The expression return key($this->elements) also could return the type string which is incompatible with the documented return type integer.
Loading history...
332
	}
333
334
	/**
335
	 * PHP Iterator Interface
336
	 *
337
	 * @see Iterator::next()
338
	 * @return mixed
339
	 */
340
	public function next() {
341
		$this->sortIfUnsorted();
342
		return next($this->elements);
343
	}
344
345
	/**
346
	 * PHP Iterator Interface
347
	 *
348
	 * @see Iterator::valid()
349
	 * @return bool
350
	 */
351
	public function valid() {
352
		$this->sortIfUnsorted();
353
		$key = key($this->elements);
354
		return ($key !== null && $key !== false);
355
	}
356
357
	/**
358
	 * Countable interface
359
	 *
360
	 * @see Countable::count()
361
	 * @return int
362
	 */
363
	public function count() {
364
		return count($this->elements);
365
	}
366
}
367