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

TWeakList::ensureDiscardInvalid()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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