Passed
Push — php8 ( 2fa786...6e3a93 )
by Fabio
12:43 queued 07:21
created

TWeakList::indexOf()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
/**
3
 * TWeakList class
4
 *
5
 * @author Brad Anderson <[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\TPropertyValue;
16
17
use ArrayAccess;
18
use Closure;
19
use Traversable;
20
use WeakReference;
21
22
/**
23
 * TWeakList class
24
 *
25
 * TWeakList implements an integer-indexed collection class with objects kept as
26
 * WeakReference.  Closure are treated as function PHP types rather than as objects.
27
 *
28
 * Objects in the TWeakList are encoded into WeakReference when saved, and the objects
29
 * restored on retrieval.  When an object becomes unset in the application/system
30
 * and its WeakReference invalidated, it can be removed from the TWeakList or have
31
 * a null in place of the object, depending on the mode.  The mode can be set during
32
 * {@link __construct Construct}.  The default mode of the TWeakList is to maintain
33
 * a list of only valid objects -where the count and item locations can change when
34
 * an item is invalidated-.  The other mode is to retain the place of invalidated
35
 * objects and replace the object with null -maintaining the count and item locations-.
36
 *
37
 * List items do not need to be objects.  TWeakList is similar to TList except list
38
 * items that are objects (except Closure) are stored as WeakReference.  List items
39
 * that are arrays are recursively traversed for replacement of objects with WeakReference
40
 * before storing.  In this way, TWeakList will not retain objects (incrementing
41
 * their use/reference counter) that it contains.  Only primary list items are tracked
42
 * with the WeakMap, and objects in arrays has no effect on the whole.   If an object
43
 * in an array is invalidated, it well be replaced by "null".  Arrays in the TWeakList
44
 * are kept regardless of the use/reference count of contained objects.
45
 *
46
 * {@link TWeakCollectionTrait} implements a PHP 8 WeakMap used to track any changes
47
 * in WeakReference objects in the TWeakList and optionally scrubs the list of invalid
48
 * objects on any changes to the WeakMap.
49
 *
50
 * Note that any objects or objects in arrays will be lost if they are not otherwise
51
 * retained in other parts of the application.  The only exception is a PHP Closure.
52
 * Closures are stored without WeakReference so anonymous functions can be stored
53
 * without risk of deletion if it is the only reference.  Closures act similarly to
54
 * a PHP data type rather than an object.
55
 *
56
 * @author Brad Anderson <[email protected]>
57
 * @since 4.2.3
58
 */
59
class TWeakList extends TList
60
{
61
	use TWeakCollectionTrait;
62
63
	/** @var bool Should invalid WeakReference automatically be deleted from the list.
64
	 *    Default True.
65
	 */
66
	private bool $_discardInvalid = true;
67
68
	/**
69
	 * Constructor.
70
	 * Initializes the weak list with an array or an iterable object.
71
	 * @param null|array|\Iterator $data The initial data. Default is null, meaning no initialization.
72
	 * @param bool $readOnly Whether the list is read-only. Default is false.
73
	 * @param ?bool $discardInvalid Whether the list is scrubbed of invalid WeakReferences.
74
	 *   Default is null for the opposite of $readOnly.  Thus, Read Only lists retain
75
	 *   invalid WeakReference; and Mutable lists scrub invalid WeakReferences.
76
	 * @throws TInvalidDataTypeException If data is not null and neither an array nor an iterator.
77
	 */
78
	public function __construct($data = null, $readOnly = false, $discardInvalid = null)
79
	{
80
		if ($discardInvalid === null) {
81
			$discardInvalid = !$readOnly;
82
		}
83
		$this->_discardInvalid = $discardInvalid;
84
		if ($discardInvalid) {
85
			$this->weakStart();
86
		}
87
		parent::__construct($data, $readOnly);
88
	}
89
90
	/**
91
	 * Cloning a TWeakList requires cloning the WeakMap
92
	 */
93
	public function __clone()
94
	{
95
		$this->weakClone();
96
		parent::__clone();
97
	}
98
99
	/**
100
	 * Waking up a TWeakList requires creating the WeakMap.  No items are saved in
101
	 * TWeakList so only initialization of the WeakMap is required.
102
	 */
103
	public function __wakeup()
104
	{
105
		if ($this->_discardInvalid) {
106
			$this->weakStart();
107
		}
108
		parent::__wakeup();
109
	}
110
111
	/**
112
	 * Converts the $item callable that has WeakReference rather than the actual object
113
	 * back into a regular callable.
114
	 * @param mixed &$item
115
	 */
116
	protected function filterItemForOutput(&$item): void
117
	{
118
		if (is_array($item) || ($item instanceof Traversable && $item instanceof ArrayAccess)) {
119
			foreach (array_keys($item) as $key) {
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type Traversable&ArrayAccess; however, parameter $array of array_keys() does only seem to accept array, 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

119
			foreach (array_keys(/** @scrutinizer ignore-type */ $item) as $key) {
Loading history...
120
				$this->filterItemForOutput($item[$key]);
121
			}
122
		} elseif (is_object($item) && ($item instanceof WeakReference)) {
123
			$item = $item->get();
124
		}
125
	}
126
127
	/**
128
	 * Converts the $item object and objects in an array into their WeakReference version
129
	 * for storage.  Closure[s] are not converted into WeakReference and so act like a
130
	 * basic PHP type.  Closures are added to the the WeakMap cache but has no weak
131
	 * effect because the TWeakList maintains references to Closure[s] preventing their
132
	 * invalidation.
133
	 * @param mixed &$item object to convert into a WeakReference where needed.
134
	 */
135
	protected function filterItemForInput(&$item): void
136
	{
137
		if (is_array($item) || ($item instanceof Traversable && $item instanceof ArrayAccess)) {
138
			foreach (array_keys($item) as $key) {
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type Traversable&ArrayAccess; however, parameter $array of array_keys() does only seem to accept array, 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

138
			foreach (array_keys(/** @scrutinizer ignore-type */ $item) as $key) {
Loading history...
139
				$this->filterItemForInput($item[$key]);
140
			}
141
		} elseif (is_object($item) && !($item instanceof Closure)) {
142
			$item = WeakReference::create($item);
143
		}
144
	}
145
146
	/**
147
	 * When a change in the WeakMap is detected, scrub the list of invalid WeakReference.
148
	 */
149
	protected function scrubWeakReferences(): void
150
	{
151
		if (!$this->_discardInvalid || !$this->weakChanged()) {
152
			return;
153
		}
154
		for ($i = $this->_c - 1; $i >= 0; $i--) {
155
			if (is_object($this->_d[$i]) && ($this->_d[$i] instanceof WeakReference) && $this->_d[$i]->get() === null) {
156
				$this->_c--;
157
				if ($i === $this->_c) {
158
					array_pop($this->_d);
159
				} else {
160
					array_splice($this->_d, $i, 1);
161
				}
162
			}
163
		}
164
		$this->weakResetCount();
165
	}
166
167
	/**
168
	 * @return bool Does the TWeakList scrub invalid WeakReference.
169
	 */
170
	public function getDiscardInvalid(): bool
171
	{
172
		return $this->_discardInvalid;
173
	}
174
175
	/**
176
	 * @param bool $value Sets the TWeakList scrubbing of invalid WeakReference.
177
	 */
178
	protected function setDiscardInvalid($value): void
179
	{
180
		$value = TPropertyValue::ensureBoolean($value);
181
		if ($value && !$this->_discardInvalid) {
182
			$this->weakStart();
183
			for ($i = $this->_c - 1; $i >= 0; $i--) {
184
				$object = false;
185
				if (is_object($this->_d[$i]) && ($this->_d[$i] instanceof WeakReference) && ($object = $this->_d[$i]->get()) !== null) {
186
					$this->weakAdd($object);
187
				}
188
				if ($object === null) {
189
					$this->_c--;	//on read only, parent::removeAt won't remove for scrub.
190
					if ($i === $this->_c) {
191
						array_pop($this->_d);
192
					} else {
193
						array_splice($this->_d, $i, 1);
194
					}
195
				}
196
			}
197
		} elseif (!$value && $this->_discardInvalid) {
198
			$this->weakStop();
199
		}
200
		$this->_discardInvalid = $value;
201
	}
202
203
	/**
204
	 * Returns an iterator for traversing the items in the list.
205
	 * This method is required by the interface \IteratorAggregate.
206
	 * All invalid WeakReference[s] are optionally removed from the iterated list.
207
	 * @return \Iterator an iterator for traversing the items in the list.
208
	 */
209
	public function getIterator(): \Iterator
210
	{
211
		return new \ArrayIterator($this->toArray());
212
	}
213
214
	/**
215
	 * All invalid WeakReference[s] are optionally removed from the list before counting.
216
	 * @return int the number of items in the list
217
	 */
218
	public function getCount(): int
219
	{
220
		$this->scrubWeakReferences();
221
		return parent::getCount();
222
	}
223
224
	/**
225
	 * Returns the item at the specified offset.
226
	 * This method is exactly the same as {@link offsetGet}.
227
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
228
	 * @param int $index the index of the item
229
	 * @throws TInvalidDataValueException if the index is out of the range
230
	 * @return mixed the item at the index
231
	 */
232
	public function itemAt($index)
233
	{
234
		$this->scrubWeakReferences();
235
		$item = parent::itemAt($index);
236
		$this->filterItemForOutput($item);
237
		return $item;
238
	}
239
240
	/**
241
	 * Appends an item at the end of the list.
242
	 * All invalid WeakReference[s] are optionally removed from the list before adding
243
	 * for proper indexing.
244
	 * @param mixed $item new item
245
	 * @throws TInvalidOperationException if the list is read-only
246
	 * @return int the zero-based index at which the item is added
247
	 */
248
	public function add($item)
249
	{
250
		$this->scrubWeakReferences();
251
		if (is_object($item)) {
252
			$this->weakAdd($item);
253
		}
254
		$this->filterItemForInput($item);
255
		parent::insertAt($this->_c, $item);
256
		return $this->_c - 1;
257
	}
258
259
	/**
260
	 * Inserts an item at the specified position.
261
	 * Original item at the position and the next items
262
	 * will be moved one step towards the end.
263
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
264
	 * @param int $index the specified position.
265
	 * @param mixed $item new item
266
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
267
	 * @throws TInvalidOperationException if the list is read-only
268
	 */
269
	public function insertAt($index, $item)
270
	{
271
		$this->scrubWeakReferences();
272
		if (is_object($item)) {
273
			$this->weakAdd($item);
274
		}
275
		$this->filterItemForInput($item);
276
		parent::insertAt($index, $item);
277
	}
278
279
	/**
280
	 * Removes an item from the list.
281
	 * The list will first search for the item.
282
	 * The first item found will be removed from the list.
283
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
284
	 * @param mixed $item the item to be removed.
285
	 * @throws TInvalidDataValueException If the item does not exist
286
	 * @throws TInvalidOperationException if the list is read-only
287
	 * @return int the index at which the item is being removed
288
	 */
289
	public function remove($item)
290
	{
291
		if (!$this->getReadOnly()) {
292
			if (($index = $this->indexOf($item)) !== -1) {
293
				if (is_object($item)) {
294
					$this->weakRemove($item);
295
				}
296
				parent::removeAt($index);
297
				return $index;
298
			} else {
299
				throw new TInvalidDataValueException('list_item_inexistent');
300
			}
301
		} else {
302
			throw new TInvalidOperationException('list_readonly', get_class($this));
303
		}
304
	}
305
306
	/**
307
	 * Removes an item at the specified position.
308
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
309
	 * @param int $index the index of the item to be removed.
310
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
311
	 * @throws TInvalidOperationException if the list is read-only
312
	 * @return mixed the removed item.
313
	 */
314
	public function removeAt($index)
315
	{
316
		$this->scrubWeakReferences();
317
		$item = parent::removeAt($index);
318
		$this->filterItemForOutput($item);
319
		if (is_object($item)) {
320
			$this->weakRemove($item);
321
		}
322
		return $item;
323
	}
324
325
	/**
326
	 * Removes all items in the list and resets the Weak Cache.
327
	 * @throws TInvalidOperationException if the list is read-only
328
	 */
329
	public function clear(): void
330
	{
331
		$c = $this->_c;
332
		for ($i = $this->_c - 1; $i >= 0; --$i) {
333
			parent::removeAt($i);
334
		}
335
		if ($c) {
336
			$this->weakRestart();
337
		}
338
	}
339
340
	/**
341
	 * @param mixed $item the item
342
	 * @return bool whether the list contains the item
343
	 */
344
	public function contains($item): bool
345
	{
346
		$this->filterItemForInput($item);
347
		return parent::indexOf($item) !== -1;
348
	}
349
350
	/**
351
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
352
	 * @param mixed $item the item
353
	 * @return int the index of the item in the list (0 based), -1 if not found.
354
	 */
355
	public function indexOf($item)
356
	{
357
		$this->scrubWeakReferences();
358
		$this->filterItemForInput($item);
359
		return parent::indexOf($item);
360
	}
361
362
	/**
363
	 * Finds the base item.  If found, the item is inserted before it.
364
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
365
	 * @param mixed $baseitem the base item which will be pushed back by the second parameter
366
	 * @param mixed $item the item
367
	 * @throws TInvalidDataValueException if the base item is not within this list
368
	 * @throws TInvalidOperationException if the list is read-only
369
	 * @return int the index where the item is inserted
370
	 */
371
	public function insertBefore($baseitem, $item)
372
	{
373
		if (!$this->getReadOnly()) {
374
			if (($index = $this->indexOf($baseitem)) === -1) {
375
				throw new TInvalidDataValueException('list_item_inexistent');
376
			}
377
			if (is_object($item)) {
378
				$this->weakAdd($item);
379
			}
380
			$this->filterItemForInput($item);
381
			parent::insertAt($index, $item);
382
			return $index;
383
		} else {
384
			throw new TInvalidOperationException('list_readonly', get_class($this));
385
		}
386
	}
387
388
	/**
389
	 * Finds the base item.  If found, the item is inserted after it.
390
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
391
	 * @param mixed $baseitem the base item which comes before the second parameter when added to the list
392
	 * @param mixed $item the item
393
	 * @throws TInvalidDataValueException if the base item is not within this list
394
	 * @throws TInvalidOperationException if the list is read-only
395
	 * @return int the index where the item is inserted
396
	 */
397
	public function insertAfter($baseitem, $item)
398
	{
399
		if (!$this->getReadOnly()) {
400
			if (($index = $this->indexOf($baseitem)) === -1) {
401
				throw new TInvalidDataValueException('list_item_inexistent');
402
			}
403
			if (is_object($item)) {
404
				$this->weakAdd($item);
405
			}
406
			$this->filterItemForInput($item);
407
			parent::insertAt($index + 1, $item);
408
			return $index + 1;
409
		} else {
410
			throw new TInvalidOperationException('list_readonly', get_class($this));
411
		}
412
	}
413
414
	/**
415
	 * All invalid WeakReference[s] are optionally removed from the list.
416
	 * @return array the list of items in array
417
	 */
418
	public function toArray(): array
419
	{
420
		$this->scrubWeakReferences();
421
		$items = $this->_d;
422
		$this->filterItemForOutput($items);
423
		return $items;
424
	}
425
426
	/**
427
	 * Copies iterable data into the list.
428
	 * Note, existing data in the list will be cleared first.
429
	 * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
430
	 * @throws TInvalidDataTypeException If data is neither an array nor a Traversable.
431
	 */
432
	public function copyFrom($data): void
433
	{
434
		if (is_array($data) || ($data instanceof Traversable)) {
435
			if ($this->_c > 0) {
436
				$this->clear();
437
			}
438
			foreach ($data as $item) {
439
				if (is_object($item)) {
440
					$this->weakAdd($item);
441
				}
442
				$this->filterItemForInput($item);
443
				parent::insertAt($this->_c, $item);
444
			}
445
		} elseif ($data !== null) {
446
			throw new TInvalidDataTypeException('list_data_not_iterable');
447
		}
448
	}
449
450
	/**
451
	 * Merges iterable data into the map.
452
	 * New data will be appended to the end of the existing data.
453
	 * @param mixed $data the data to be merged with, must be an array or object implementing Traversable
454
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
455
	 */
456
	public function mergeWith($data): void
457
	{
458
		if (is_array($data) || ($data instanceof Traversable)) {
459
			foreach ($data as $item) {
460
				if (is_object($item)) {
461
					$this->weakAdd($item);
462
				}
463
				$this->filterItemForInput($item);
464
				parent::insertAt($this->_c, $item);
465
			}
466
		} elseif ($data !== null) {
467
			throw new TInvalidDataTypeException('list_data_not_iterable');
468
		}
469
	}
470
471
	/**
472
	 * Returns whether there is an item at the specified offset.
473
	 * This method is required by the interface \ArrayAccess.
474
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
475
	 * @param int $offset the offset to check on
476
	 * @return bool
477
	 */
478
	public function offsetExists($offset): bool
479
	{
480
		return ($offset >= 0 && $offset < $this->getCount());
481
	}
482
483
	/**
484
	 * Sets the item at the specified offset.
485
	 * This method is required by the interface \ArrayAccess.
486
	 * All invalid WeakReference[s] are optionally removed from the list before indexing.
487
	 * @param int $offset the offset to set item
488
	 * @param mixed $item the item value
489
	 */
490
	public function offsetSet($offset, $item): void
491
	{
492
		$this->scrubWeakReferences();
493
		if ($offset === null || $offset === $this->_c) {
494
			if (is_object($item)) {
495
				$this->weakAdd($item);
496
			}
497
			$this->filterItemForInput($item);
498
			parent::insertAt($this->_c, $item);
499
		} else {
500
			$removed = parent::removeAt($offset);
501
			$this->filterItemForOutput($removed);
502
			if (is_object($removed)) {
503
				$this->weakRemove($removed);
504
			}
505
			if (is_object($item)) {
506
				$this->weakAdd($item);
507
			}
508
			$this->filterItemForInput($item);
509
			parent::insertAt($offset, $item);
510
		}
511
	}
512
513
	/**
514
	 * Returns an array with the names of all variables of this object that should
515
	 * NOT be serialized because their value is the default one or useless to be cached
516
	 * for the next page loads.  Reimplement in derived classes to add new variables,
517
	 * but remember to  also to call the parent implementation first.
518
	 * Due to being weak, the TWeakList is not serialized.  The count is artificially
519
	 * made zero so the parent has no values to save.
520
	 * @param array $exprops by reference
521
	 */
522
	protected function _getZappableSleepProps(&$exprops)
523
	{
524
		$c = $this->_c;
525
		$this->_c = 0;
526
		parent::_getZappableSleepProps($exprops);
527
		$this->_c = $c;
528
529
		$this->_weakZappableSleepProps($exprops);
530
		if ($this->_discardInvalid === true) {
531
			$exprops[] = "\0" . __CLASS__ . "\0_discardInvalid";
532
		}
533
	}
534
}
535