Passed
Push — master ( 4fc6c5...494471 )
by Fabio
07:54 queued 03:10
created

TList::collapseReadOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * TList class
4
 *
5
 * @author Qiang Xue <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Collections;
11
12
use Prado\Exceptions\TInvalidOperationException;
13
use Prado\Exceptions\TInvalidDataTypeException;
14
use Prado\Exceptions\TInvalidDataValueException;
15
use Prado\Prado;
16
use Prado\TPropertyValue;
17
18
/**
19
 * TList class
20
 *
21
 * TList implements an integer-indexed collection class.
22
 *
23
 * You can access, append, insert, remove an item by using
24
 * {@link itemAt}, {@link add}, {@link insertAt}, {@link remove}, and {@link removeAt}.
25
 * To get the number of the items in the list, use {@link getCount}.
26
 * TList can also be used like a regular array as follows,
27
 * <code>
28
 * $list[]=$item;  // append at the end
29
 * $list[$index]=$item; // $index must be between 0 and $list->Count
30
 * unset($list[$index]); // remove the item at $index
31
 * if(isset($list[$index])) // if the list has an item at $index
32
 * foreach($list as $index=>$item) // traverse each item in the list
33
 * $n=count($list); // returns the number of items in the list
34
 * </code>
35
 *
36
 * To extend TList by doing additional operations with each addition or removal
37
 * operation, override {@link insertAt()}, and {@link removeAt()}.
38
 *
39
 * @author Qiang Xue <[email protected]>
40
 * @since 3.0
41
 */
42
class TList extends \Prado\TComponent implements \IteratorAggregate, \ArrayAccess, \Countable
43
{
44
	/**
45
	 * internal data storage
46
	 * @var array
47
	 */
48
	protected array $_d = [];
49
	/**
50
	 * number of items
51
	 * @var int
52
	 */
53
	protected int $_c = 0;
54
	/**
55
	 * @var ?bool whether this list is read-only
56
	 */
57
	protected ?bool $_r = null;
58
59
	/**
60
	 * Constructor.
61
	 * Initializes the list with an array or an iterable object.
62
	 * @param null|array|\Iterator $data the initial data. Default is null, meaning no initialization.
63
	 * @param ?bool $readOnly whether the list is read-only, default null.
64
	 * @throws TInvalidDataTypeException If data is not null and neither an array nor an iterator.
65
	 */
66
	public function __construct($data = null, $readOnly = null)
67 206
	{
68
		parent::__construct();
69 206
		if ($data !== null) {
70 29
			$this->copyFrom($data);
71
			$readOnly = (bool) $readOnly;
72 206
		}
73 206
		$this->setReadOnly($readOnly);
74
	}
75
76
	/**
77
	 * @return bool whether this list is read-only or not. Defaults to false.
78 120
	 */
79
	public function getReadOnly(): bool
80 120
	{
81
		return (bool) $this->_r;
82
	}
83
84
	/**
85
	 * @param null|bool|string $value whether this list is read-only or not
86 206
	 */
87
	public function setReadOnly($value)
88 206
	{
89 206
		if ($value === null) {
90
			return;
91
		}
92
		if($this->_r === null || Prado::isCallingSelf()) {
93
			$this->_r = TPropertyValue::ensureBoolean($value);
94
		} else {
95
			throw new TInvalidOperationException('list_readonly_set', $this::class);
96 25
		}
97
	}
98 25
99
	/**
100
	 * This sets the read only property.
101
	 */
102
	protected function collapseReadOnly(): void
103
	{
104
		$this->_r = (bool) $this->_r;
105
	}
106 2
107
	/**
108 2
	 * Returns an iterator for traversing the items in the list.
109
	 * This method is required by the interface \IteratorAggregate.
110
	 * @return \Iterator an iterator for traversing the items in the list.
111
	 */
112
	public function getIterator(): \Iterator
113
	{
114 151
		return new \ArrayIterator($this->_d);
115
	}
116 151
117
	/**
118
	 * Returns the number of items in the list.
119
	 * This method is required by \Countable interface.
120
	 * @return int number of items in the list.
121
	 */
122
	public function count(): int
123
	{
124
		return $this->getCount();
125
	}
126 102
127
	/**
128 102
	 * @return int the number of items in the list
129 102
	 */
130
	public function getCount(): int
131 2
	{
132
		return $this->_c;
133
	}
134
135
	/**
136
	 * Returns the item at the specified offset.
137
	 * This method is exactly the same as {@link offsetGet}.
138
	 * @param int $index the index of the item
139
	 * @throws TInvalidDataValueException if the index is out of the range
140
	 * @return mixed the item at the index
141 79
	 */
142
	public function itemAt($index)
143 79
	{
144 79
		if ($index >= 0 && $index < $this->_c) {
145
			return $this->_d[$index];
146
		} else {
147
			throw new TInvalidDataValueException('list_index_invalid', $index);
148
		}
149
	}
150
151
	/**
152
	 * Appends an item at the end of the list.
153
	 * @param mixed $item new item
154
	 * @throws TInvalidOperationException if the list is read-only
155
	 * @return int the zero-based index at which the item is added
156 100
	 */
157
	public function add($item)
158 100
	{
159 100
		$this->insertAt($this->_c, $item);
160 100
		return $this->_c - 1;
161 2
	}
162 2
163 2
	/**
164
	 * Inserts an item at the specified position.
165 100
	 * Original item at the position and the next items
166
	 * will be moved one step towards the end.
167
	 * @param int $index the specified position.
168 2
	 * @param mixed $item new item
169
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
170 100
	 * @throws TInvalidOperationException if the list is read-only
171
	 */
172
	public function insertAt($index, $item)
173
	{
174
		$this->collapseReadOnly();
175
		if (!$this->_r) {
176
			if ($index === $this->_c) {
177
				$this->_d[$this->_c++] = $item;
178
			} elseif ($index >= 0 && $index < $this->_c) {
179
				array_splice($this->_d, $index, 0, [$item]);
180
				$this->_c++;
181 8
			} else {
182
				throw new TInvalidDataValueException('list_index_invalid', $index);
183 8
			}
184 7
		} else {
185 7
			throw new TInvalidOperationException('list_readonly', $this::class);
186 7
		}
187
	}
188 1
189
	/**
190
	 * Removes an item from the list.
191 1
	 * The list will first search for the item.
192
	 * The first item found will be removed from the list.
193
	 * @param mixed $item the item to be removed.
194
	 * @throws TInvalidDataValueException If the item does not exist
195
	 * @throws TInvalidOperationException if the list is read-only
196
	 * @return int the index at which the item is being removed
197
	 */
198
	public function remove($item)
199
	{
200
		if (!$this->_r) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_r of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
201
			if (($index = $this->indexOf($item)) >= 0) {
202 18
				$this->removeAt($index);
203
				return $index;
204 18
			} else {
205 16
				throw new TInvalidDataValueException('list_item_inexistent');
206 16
			}
207 16
		} else {
208 11
			throw new TInvalidOperationException('list_readonly', $this::class);
209
		}
210 6
	}
211 6
212 6
	/**
213
	 * Removes an item at the specified position.
214
	 * @param int $index the index of the item to be removed.
215 2
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
216
	 * @throws TInvalidOperationException if the list is read-only
217
	 * @return mixed the removed item.
218 2
	 */
219
	public function removeAt($index)
220
	{
221
		if (!$this->_r) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_r of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
222
			if ($index >= 0 && $index < $this->_c) {
223
				$this->_c--;
224
				if ($index === $this->_c) {
225
					return array_pop($this->_d);
226 17
				} else {
227
					$item = $this->_d[$index];
228 17
					array_splice($this->_d, $index, 1);
229 7
					return $item;
230
				}
231 16
			} else {
232
				throw new TInvalidDataValueException('list_index_invalid', $index);
233
			}
234
		} else {
235
			throw new TInvalidOperationException('list_readonly', $this::class);
236
		}
237 8
	}
238
239 8
	/**
240
	 * Removes all items in the list.
241
	 * @throws TInvalidOperationException if the list is read-only
242
	 */
243
	public function clear(): void
244
	{
245
		for ($i = $this->_c - 1; $i >= 0; --$i) {
246 16
			$this->removeAt($i);
247
		}
248 16
	}
249 13
250
	/**
251 12
	 * @param mixed $item the item
252
	 * @return bool whether the list contains the item
253
	 */
254
	public function contains($item): bool
255
	{
256
		return $this->indexOf($item) !== -1;
257
	}
258
259
	/**
260
	 * @param mixed $item the item
261
	 * @return int the index of the item in the list (0 based), -1 if not found.
262
	 */
263
	public function indexOf($item)
264 2
	{
265
		if (($index = array_search($item, $this->_d, true)) === false) {
266 2
			return -1;
267 1
		} else {
268 1
			return $index;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $index also could return the type string which is incompatible with the documented return type integer.
Loading history...
269
		}
270
	}
271
272
	/**
273
	 * Finds the base item.  If found, the item is inserted before it.
274
	 * @param mixed $baseitem the base item which will be pushed back by the second parameter
275 1
	 * @param mixed $item the item
276
	 * @throws TInvalidDataValueException if the base item is not within this list
277
	 * @throws TInvalidOperationException if the list is read-only
278
	 * @return int the index where the item is inserted
279
	 * @since 3.2a
280
	 */
281
	public function insertBefore($baseitem, $item)
282
	{
283
		if (!$this->_r) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_r of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
284
			if (($index = $this->indexOf($baseitem)) == -1) {
285
				throw new TInvalidDataValueException('list_item_inexistent');
286
			}
287
288 2
			$this->insertAt($index, $item);
289
290 2
			return $index;
291 1
		} else {
292 1
			throw new TInvalidOperationException('list_readonly', $this::class);
293
		}
294
	}
295
296
	/**
297
	 * Finds the base item.  If found, the item is inserted after it.
298
	 * @param mixed $baseitem the base item which comes before the second parameter when added to the list
299 1
	 * @param mixed $item the item
300
	 * @throws TInvalidDataValueException if the base item is not within this list
301
	 * @throws TInvalidOperationException if the list is read-only
302
	 * @return int the index where the item is inserted
303
	 * @since 3.2a
304
	 */
305
	public function insertAfter($baseitem, $item)
306 8
	{
307
		if (!$this->_r) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_r of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
308 8
			if (($index = $this->indexOf($baseitem)) == -1) {
309
				throw new TInvalidDataValueException('list_item_inexistent');
310
			}
311
312
			$this->insertAt($index + 1, $item);
313
314
			return $index + 1;
315
		} else {
316
			throw new TInvalidOperationException('list_readonly', $this::class);
317 31
		}
318
	}
319 31
320 31
	/**
321 2
	 * @return array the list of items in array
322
	 */
323 31
	public function toArray(): array
324 31
	{
325
		return $this->_d;
326 1
	}
327 1
328
	/**
329 31
	 * Copies iterable data into the list.
330
	 * Note, existing data in the list will be cleared first.
331
	 * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
332
	 * @throws TInvalidDataTypeException If data is neither an array nor a Traversable.
333
	 */
334
	public function copyFrom($data): void
335
	{
336
		if (is_array($data) || ($data instanceof \Traversable)) {
337 1
			if ($this->_c > 0) {
338
				$this->clear();
339 1
			}
340 1
			foreach ($data as $item) {
341 1
				$this->add($item);
342
			}
343 1
		} elseif ($data !== null) {
344 1
			throw new TInvalidDataTypeException('list_data_not_iterable');
345
		}
346 1
	}
347
348
	/**
349
	 * Merges iterable data into the map.
350
	 * New data will be appended to the end of the existing data.
351
	 * @param mixed $data the data to be merged with, must be an array or object implementing Traversable
352
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
353
	 */
354 1
	public function mergeWith($data): void
355
	{
356 1
		if (is_array($data) || ($data instanceof \Traversable)) {
357
			foreach ($data as $item) {
358
				$this->add($item);
359
			}
360
		} elseif ($data !== null) {
361
			throw new TInvalidDataTypeException('list_data_not_iterable');
362
		}
363
	}
364
365
	/**
366 8
	 * Returns whether there is an item at the specified offset.
367
	 * This method is required by the interface \ArrayAccess.
368 8
	 * @param int $offset the offset to check on
369
	 * @return bool
370
	 */
371
	public function offsetExists($offset): bool
372
	{
373
		return ($offset >= 0 && $offset < $this->_c);
374
	}
375
376
	/**
377 18
	 * Returns the item at the specified offset.
378
	 * This method is required by the interface \ArrayAccess.
379 18
	 * @param int $offset the offset to retrieve item.
380 17
	 * @throws TInvalidDataValueException if the offset is invalid
381
	 * @return mixed the item at the offset
382 1
	 */
383 1
	public function offsetGet($offset): mixed
384
	{
385 18
		return $this->itemAt($offset);
386
	}
387
388
	/**
389
	 * Sets the item at the specified offset.
390
	 * This method is required by the interface \ArrayAccess.
391
	 * @param int $offset the offset to set item
392 1
	 * @param mixed $item the item value
393
	 */
394 1
	public function offsetSet($offset, $item): void
395 1
	{
396
		if ($offset === null || $offset === $this->_c) {
397
			$this->insertAt($this->_c, $item);
398
		} else {
399
			$this->removeAt($offset);
400
			$this->insertAt($offset, $item);
401
		}
402
	}
403
404
	/**
405
	 * Unsets the item at the specified offset.
406
	 * This method is required by the interface \ArrayAccess.
407
	 * @param int $offset the offset to unset item
408
	 */
409
	public function offsetUnset($offset): void
410
	{
411
		$this->removeAt($offset);
412
	}
413
414
	/**
415
	 * Returns an array with the names of all variables of this object that should NOT be serialized
416
	 * because their value is the default one or useless to be cached for the next page loads.
417
	 * When there are no items in the list, _d and _c are not stored
418
	 * @param array $exprops by reference
419
	 * @since 4.2.3
420
	 */
421
	protected function _getZappableSleepProps(&$exprops)
422
	{
423
		parent::_getZappableSleepProps($exprops);
424
		if ($this->_c === 0) {
425
			$exprops[] = "\0*\0_d";
426
			$exprops[] = "\0*\0_c";
427
		}
428
		if ($this->_r === null) {
429
			$exprops[] = "\0" . __CLASS__ . "\0_r";
430
		}
431
	}
432
}
433