Passed
Push — master ( 246025...18c029 )
by Fabio
05:31
created

TMap   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Test Coverage

Coverage 90.54%

Importance

Changes 0
Metric Value
eloc 83
dl 0
loc 318
rs 6.96
c 0
b 0
f 0
ccs 67
cts 74
cp 0.9054
wmc 53

23 Methods

Rating   Name   Duplication   Size   Complexity  
A getKeys() 0 3 1
A getIterator() 0 3 1
A getCount() 0 3 1
A count() 0 3 1
A setReadOnly() 0 9 4
A __construct() 0 8 2
A getReadOnly() 0 3 1
A collapseReadOnly() 0 3 1
A copyFrom() 0 11 6
A itemAt() 0 3 3
A contains() 0 3 2
A remove() 0 13 4
A mergeWith() 0 8 5
A offsetGet() 0 3 1
A removeItem() 0 12 4
A offsetExists() 0 3 1
A toArray() 0 3 1
A clear() 0 4 2
A keyOf() 0 12 4
A offsetUnset() 0 3 1
A offsetSet() 0 3 1
A add() 0 14 3
A _getZappableSleepProps() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like TMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TMap, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * TMap 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\TInvalidDataTypeException;
13
use Prado\Exceptions\TInvalidOperationException;
14
use Prado\Prado;
15
use Prado\TPropertyValue;
16
use Traversable;
17
18
/**
19
 * TMap class
20
 *
21
 * TMap implements a collection that takes key-value pairs.
22
 *
23
 * You can access, add or remove an item with a key by using
24
 * {@link itemAt}, {@link add}, and {@link remove}.
25
 * To get the number of the items in the map, use {@link getCount}.
26
 * TMap can also be used like a regular array as follows,
27
 * <code>
28
 * $map[$key]=$value; // add a key-value pair
29
 * unset($map[$key]); // remove the value with the specified key
30
 * if(isset($map[$key])) // if the map contains the key
31
 * foreach($map as $key=>$value) // traverse the items in the map
32
 * $n=count($map);  // returns the number of items in the map
33
 * </code>
34
 *
35
 * @author Qiang Xue <[email protected]>
36
 * @since 3.0
37
 * @method void dyAddItem(mixed $key, mixed $value)
38
 * @method void dyRemoveItem(mixed $key, mixed $value)
39
 * @method mixed dyNoItem(mixed $returnValue, mixed $key)
40
 */
41
class TMap extends \Prado\TComponent implements \IteratorAggregate, \ArrayAccess, \Countable
42
{
43
	/**
44
	 * @var array<int|string, mixed> internal data storage
45
	 */
46
	protected array $_d = [];
47
	/**
48
	 * @var ?bool whether this list is read-only
49
	 */
50
	private ?bool $_r = null;
51
52
	/**
53
	 * Constructor.
54
	 * Initializes the list with an array or an iterable object.
55
	 * @param null|array|\Iterator $data the intial data. Default is null, meaning no initialization.
56
	 * @param ?bool $readOnly whether the list is read-only, default null.
57
	 * @throws TInvalidDataTypeException If data is not null and neither an array nor an iterator.
58
	 */
59
	public function __construct($data = null, $readOnly = null)
60
	{
61
		parent::__construct();
62
		if ($data !== null) {
63
			$this->copyFrom($data);
64
			$readOnly = (bool) $readOnly;
65
		}
66
		$this->setReadOnly($readOnly);
67
	}
68
69
	/**
70
	 * @return bool whether this map is read-only or not. Defaults to false.
71
	 */
72
	public function getReadOnly(): bool
73
	{
74
		return (bool) $this->_r;
75 203
	}
76
77 203
	/**
78 8
	 * @param null|bool|string $value whether this list is read-only or not
79
	 */
80 203
	public function setReadOnly($value)
81 203
	{
82
		if ($value === null) {
83
			return;
84
		}
85
		if($this->_r === null || Prado::isCallingSelf()) {
86 2
			$this->_r = TPropertyValue::ensureBoolean($value);
87
		} else {
88 2
			throw new TInvalidOperationException('map_readonly_set', $this::class);
89
		}
90
	}
91
92
	/**
93
	 * This sets the read only property.
94 256
	 */
95
	protected function collapseReadOnly(): void
96 256
	{
97 256
		$this->_r = (bool) $this->_r;
98
	}
99
100
	/**
101
	 * Returns an iterator for traversing the items in the list.
102
	 * This method is required by the interface \IteratorAggregate.
103
	 * @return \Iterator an iterator for traversing the items in the list.
104 93
	 */
105
	public function getIterator(): \Iterator
106 93
	{
107
		return new \ArrayIterator($this->_d);
108
	}
109
110
	/**
111
	 * Returns the number of items in the map.
112
	 * This method is required by \Countable interface.
113
	 * @return int number of items in the map.
114 32
	 */
115
	public function count(): int
116 32
	{
117
		return $this->getCount();
118
	}
119
120
	/**
121
	 * @return int the number of items in the map
122 72
	 */
123
	public function getCount(): int
124 72
	{
125
		return count($this->_d);
126
	}
127
128
	/**
129
	 * @return array<int|string> the key list
130 5
	 */
131
	public function getKeys(): array
132 5
	{
133
		return array_keys($this->_d);
134
	}
135
136
	/**
137
	 * Returns the item with the specified key.
138
	 * This method is exactly the same as {@link offsetGet}.
139
	 * @param mixed $key the key
140
	 * @return mixed the element at the offset, null if no element is found at the offset
141 185
	 */
142
	public function itemAt($key)
143 185
	{
144
		return (isset($this->_d[$key]) || array_key_exists($key, $this->_d)) ? $this->_d[$key] : $this->dyNoItem(null, $key);
145
	}
146
147
	/**
148
	 * Adds an item into the map.
149
	 * Note, if the specified key already exists, the old value will be overwritten.
150
	 * @param mixed $key
151
	 * @param mixed $value
152
	 * @throws TInvalidOperationException if the map is read-only
153 102
	 * @return mixed The key of the item, which is calculated when the $key is null.
154
	 */
155 102
	public function add($key, $value): mixed
156 101
	{
157
		$this->collapseReadOnly();
158 2
		if (!$this->_r) {
159
			if ($key === null) {
160 101
				$this->_d[] = $value;
161
				$key = array_key_last($this->_d);
162
			} else {
163
				$this->_d[$key] = $value;
164
			}
165
			$this->dyAddItem($key, $value);
166
			return $key;
167
		} else {
168 12
			throw new TInvalidOperationException('map_readonly', $this::class);
169
		}
170 12
	}
171 11
172 10
	/**
173 10
	 * Removes an item from the map by its key.
174 10
	 * @param mixed $key the key of the item to be removed
175
	 * @throws TInvalidOperationException if the map is read-only
176 2
	 * @return mixed the removed value, null if no such key exists.
177
	 */
178
	public function remove($key)
179 1
	{
180
		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...
181
			if (isset($this->_d[$key]) || array_key_exists($key, $this->_d)) {
182
				$value = $this->_d[$key];
183
				unset($this->_d[$key]);
184
				$this->dyRemoveItem($key, $value);
185
				return $value;
186 14
			} else {
187
				return null;
188 14
			}
189 5
		} else {
190
			throw new TInvalidOperationException('map_readonly', $this::class);
191 14
		}
192
	}
193
194
	/**
195
	 * Removes an item from the map.  This removes all of an item from the map.
196
	 * @param mixed $item the item to be removed
197 133
	 * @throws TInvalidOperationException if the map is read-only
198
	 * @return array The array of keys and the item removed.
199 133
	 * since 4.2.3
200
	 */
201
	public function removeItem(mixed $item): array
202
	{
203
		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...
204
			$return = [];
205 34
			foreach ($this->toArray() as $key => $value) {
206
				if ($item === $value) {
207 34
					$return[$key] = $this->remove($key);
208
				}
209
			}
210
			return $return;
211
		} else {
212
			throw new TInvalidOperationException('map_readonly', $this::class);
213
		}
214
	}
215
216 62
	/**
217
	 * Removes all items in the map.
218 62
	 */
219 62
	public function clear(): void
220 1
	{
221
		foreach (array_keys($this->_d) as $key) {
222 62
			$this->remove($key);
223 62
		}
224
	}
225 1
226 1
	/**
227
	 * @param mixed $key the key
228 62
	 * @return bool whether the map contains an item with the specified key
229
	 */
230
	public function contains($key): bool
231
	{
232
		return isset($this->_d[$key]) || array_key_exists($key, $this->_d);
233
	}
234
235
	/**
236 1
	 * @param mixed $item the item
237
	 * @param bool $multiple Return an array of all the keys. Default true.
238 1
	 * @return false|mixed the key of the item in the map, false if not found.
239 1
	 * since 4.2.3
240 1
	 */
241
	public function keyOf($item, bool $multiple = true): mixed
242 1
	{
243 1
		if ($multiple) {
244
			$return = [];
245 1
			foreach ($this->toArray() as $key => $value) {
246
				if ($item === $value) {
247
					$return[$key] = $item;
248
				}
249
			}
250
			return $return;
251
		} else {
252
			return array_search($item, $this->_d, true);
253 29
		}
254
	}
255 29
256
	/**
257
	 * @return array<int|string, mixed> the list of items in array
258
	 */
259
	public function toArray(): array
260
	{
261
		return $this->_d;
262
	}
263
264 157
	/**
265
	 * Copies iterable data into the map.
266 157
	 * Note, existing data in the map will be cleared first.
267
	 * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
268
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
269
	 */
270
	public function copyFrom($data): void
271
	{
272
		if (is_array($data) || $data instanceof Traversable) {
273
			if ($this->getCount() > 0) {
274
				$this->clear();
275 21
			}
276
			foreach ($data as $key => $value) {
277 21
				$this->add($key, $value);
278 21
			}
279
		} elseif ($data !== null) {
280
			throw new TInvalidDataTypeException('map_data_not_iterable');
281
		}
282
	}
283
284
	/**
285 2
	 * Merges iterable data into the map.
286
	 * Existing data in the map will be kept and overwritten if the keys are the same.
287 2
	 * @param mixed $data the data to be merged with, must be an array or object implementing Traversable
288 2
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
289
	 */
290
	public function mergeWith($data): void
291
	{
292
		if (is_array($data) || $data instanceof Traversable) {
293
			foreach ($data as $key => $value) {
294
				$this->add($key, $value);
295
			}
296
		} elseif ($data !== null) {
297
			throw new TInvalidDataTypeException('map_data_not_iterable');
298
		}
299
	}
300
301
	/**
302
	 * Returns whether there is an element at the specified offset.
303
	 * This method is required by the interface \ArrayAccess.
304
	 * @param mixed $offset the offset to check on
305
	 * @return bool
306
	 */
307
	public function offsetExists($offset): bool
308
	{
309
		return $this->contains($offset);
310
	}
311
312
	/**
313
	 * Returns the element at the specified offset.
314
	 * This method is required by the interface \ArrayAccess.
315
	 * @param mixed $offset the offset to retrieve element.
316
	 * @return mixed the element at the offset, null if no element is found at the offset
317
	 */
318
	public function offsetGet($offset): mixed
319
	{
320
		return $this->itemAt($offset);
321
	}
322
323
	/**
324
	 * Sets the element at the specified offset.
325
	 * This method is required by the interface \ArrayAccess.
326
	 * @param mixed $offset the offset to set element
327
	 * @param mixed $item the element value
328
	 */
329
	public function offsetSet($offset, $item): void
330
	{
331
		$this->add($offset, $item);
332
	}
333
334
	/**
335
	 * Unsets the element at the specified offset.
336
	 * This method is required by the interface \ArrayAccess.
337
	 * @param mixed $offset the offset to unset element
338
	 */
339
	public function offsetUnset($offset): void
340
	{
341
		$this->remove($offset);
342
	}
343
344
	/**
345
	 * Returns an array with the names of all variables of this object that should NOT be serialized
346
	 * because their value is the default one or useless to be cached for the next page loads.
347
	 * Reimplement in derived classes to add new variables, but remember to  also to call the parent
348
	 * implementation first.
349
	 * @param array $exprops by reference
350
	 */
351
	protected function _getZappableSleepProps(&$exprops)
352
	{
353
		parent::_getZappableSleepProps($exprops);
354
		if (empty($this->_d)) {
355
			$exprops[] = "\0*\0_d";
356
		}
357
		if ($this->_r === null) {
358
			$exprops[] = "\0" . __CLASS__ . "\0_r";
359
		}
360
	}
361
}
362