Passed
Pull Request — master (#892)
by
unknown
09:44 queued 04:31
created

TWeakList::scrubWeakReferences()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 10
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 16
rs 8.4444
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 true.
75
	 * @throws TInvalidDataTypeException If data is not null and neither an array nor an iterator.
76
	 */
77
	public function __construct($data = null, $readOnly = false, $discardInvalid = true)
78
	{
79
		$this->_discardInvalid = $discardInvalid;
80
		if ($discardInvalid) {
81
			$this->weakStart();
82
		}
83
		parent::__construct($data, $readOnly);
84
	}
85
86
	/**
87
	 * Cloning a TWeakList requires cloning the WeakMap
88
	 */
89
	public function __clone()
90
	{
91
		$this->weakClone();
92
		parent::__clone();
93
	}
94
95
	/**
96
	 * Waking up a TWeakList requires creating the WeakMap.  No items are saved in
97
	 * TWeakList so only initialization of the WeakMap is required.
98
	 */
99
	public function __wakeup()
100
	{
101
		if ($this->_discardInvalid) {
102
			$this->weakStart();
103
		}
104
		parent::__wakeup();
105
	}
106
107
	/**
108
	 * Converts the $item callable that has WeakReference rather than the actual object
109
	 * back into a regular callable.
110
	 * @param mixed &$item
111
	 */
112
	protected function filterItemForOutput(&$item): void
113
	{
114
		if (is_array($item) || ($item instanceof Traversable && $item instanceof ArrayAccess)) {
115
			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

115
			foreach (array_keys(/** @scrutinizer ignore-type */ $item) as $key) {
Loading history...
116
				$this->filterItemForOutput($item[$key]);
117
			}
118
		} elseif (is_object($item) && ($item instanceof WeakReference)) {
119
			$item = $item->get();
120
		}
121
	}
122
123
	/**
124
	 * Converts the $item object and objects in an array into their WeakReference version
125
	 * for storage.  Closure[s] are not converted into WeakReference and so act like a
126
	 * basic PHP type.  Closures are added to the the WeakMap cache but has no weak
127
	 * effect because the TWeakList maintains references to Closure[s] preventing their
128
	 * invalidation.
129
	 * @param mixed &$item object to convert into a WeakReference where needed.
130
	 */
131
	protected function filterItemForInput(&$item): void
132
	{
133
		if (is_array($item) || ($item instanceof Traversable && $item instanceof ArrayAccess)) {
134
			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

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