Passed
Push — 3.x ( b32bc5...54dc6d )
by Jeroen
230:33 queued 158:22
created

engine/classes/ElggPriorityList.php (1 issue)

Labels
Severity
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
	 * Has the list already been sorted
107
	 *
108
	 * @var bool
109
	 */
110
	private $sorted = false;
111
112
	/**
113
	 * Create a new priority list.
114
	 *
115
	 * @param array $elements An optional array of priorities => element
116
	 */
117 117
	public function __construct(array $elements = []) {
118 117
		foreach ($elements as $priority => $element) {
119 3
			$this->add($element, $priority);
120
		}
121 117
	}
122
123
	/**
124
	 * Adds an element to the list.
125
	 *
126
	 * @warning This returns the priority at which the element was added, which can be 0. Use
127
	 *          !== false to check for success.
128
	 *
129
	 * @param mixed $element  The element to add to the list.
130
	 * @param mixed $priority Priority to add the element. In priority collisions, the original element
131
	 *                        maintains its priority and the new element is to the next available
132
	 *                        slot, taking into consideration all previously registered elements.
133
	 *                        Negative elements are accepted.
134
	 * @param bool  $exact    unused
135
	 *
136
	 * @return int|false The priority of the added element
137
	 *
138
	 * @todo remove $exact or implement it. Note we use variable name strict below.
139
	 */
140 131
	public function add($element, $priority = null, $exact = false) {
141 131
		if ($priority !== null && !is_numeric($priority)) {
142
			return false;
143
		} else {
144 131
			$priority = $this->getNextPriority($priority);
145
		}
146
147 131
		$this->elements[$priority] = $element;
148 131
		$this->sorted = false;
149
		
150 131
		return $priority;
151
	}
152
153
	/**
154
	 * Removes an element from the list.
155
	 *
156
	 * @warning The element must have the same attributes / values. If using $strict, it must have
157
	 *          the same types. array(10) will fail in strict against array('10') (str vs int).
158
	 *
159
	 * @param mixed $element The element to remove from the list
160
	 * @param bool  $strict  Whether to check the type of the element match
161
	 *
162
	 * @return bool
163
	 */
164 4
	public function remove($element, $strict = false) {
165 4
		$index = array_search($element, $this->elements, $strict);
166 4
		if ($index === false) {
167
			return false;
168
		}
169
		
170 4
		unset($this->elements[$index]);
171 4
		return true;
172
	}
173
174
	/**
175
	 * Move an existing element to a new priority.
176
	 *
177
	 * @param mixed $element      The element to move
178
	 * @param int   $new_priority The new priority for the element
179
	 * @param bool  $strict       Whether to check the type of the element match
180
	 *
181
	 * @return bool
182
	 */
183 16
	public function move($element, $new_priority, $strict = false) {
184 16
		$new_priority = (int) $new_priority;
185
		
186 16
		$current_priority = $this->getPriority($element, $strict);
187 16
		if ($current_priority === false) {
188
			return false;
189
		}
190
191 16
		if ($current_priority == $new_priority) {
192
			return true;
193
		}
194
195
		// move the actual element so strict operations still work
196 16
		$element = $this->getElement($current_priority);
0 ignored issues
show
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

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