EnumMap.php
11.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
<?php
declare(strict_types = 1);
namespace DASPRiD\Enum;
use DASPRiD\Enum\Exception\ExpectationException;
use DASPRiD\Enum\Exception\IllegalArgumentException;
use IteratorAggregate;
use Serializable;
use Traversable;
/**
* A specialized map implementation for use with enum type keys.
*
* All of the keys in an enum map must come from a single enum type that is specified, when the map is created. Enum
* maps are represented internally as arrays. This representation is extremely compact and efficient.
*
* Enum maps are maintained in the natural order of their keys (the order in which the enum constants are declared).
* This is reflected in the iterators returned by the collection views {@see self::getIterator()} and
* {@see self::values()}.
*
* Iterators returned by the collection views are not consistent: They may or may not show the effects of modifications
* to the map that occur while the iteration is in progress.
*/
final class EnumMap implements Serializable, IteratorAggregate
{
/**
* The class name of the key.
*
* @var string
*/
private $keyType;
/**
* The type of the value.
*
* @var string
*/
private $valueType;
/**
* @var bool
*/
private $allowNullValues;
/**
* All of the constants comprising the enum, cached for performance.
*
* @var array<int, AbstractEnum>
*/
private $keyUniverse;
/**
* Array representation of this map. The ith element is the value to which universe[i] is currently mapped, or null
* if it isn't mapped to anything, or NullValue if it's mapped to null.
*
* @var array<int, mixed>
*/
private $values;
/**
* @var int
*/
private $size = 0;
/**
* Creates a new enum map.
*
* @param string $keyType the type of the keys, must extend AbstractEnum
* @param string $valueType the type of the values
* @param bool $allowNullValues whether to allow null values
* @throws IllegalArgumentException when key type does not extend AbstractEnum
*/
public function __construct(string $keyType, string $valueType, bool $allowNullValues)
{
if (! is_subclass_of($keyType, AbstractEnum::class)) {
throw new IllegalArgumentException(sprintf(
'Class %s does not extend %s',
$keyType,
AbstractEnum::class
));
}
$this->keyType = $keyType;
$this->valueType = $valueType;
$this->allowNullValues = $allowNullValues;
$this->keyUniverse = $keyType::values();
$this->values = array_fill(0, count($this->keyUniverse), null);
}
/**
* Checks whether the map types match the supplied ones.
*
* You should call this method when an EnumMap is passed to you and you want to ensure that it's made up of the
* correct types.
*
* @throws ExpectationException when supplied key type mismatches local key type
* @throws ExpectationException when supplied value type mismatches local value type
* @throws ExpectationException when the supplied map allows null values, abut should not
*/
public function expect(string $keyType, string $valueType, bool $allowNullValues) : void
{
if ($keyType !== $this->keyType) {
throw new ExpectationException(sprintf(
'Callee expected an EnumMap with key type %s, but got %s',
$keyType,
$this->keyType
));
}
if ($valueType !== $this->valueType) {
throw new ExpectationException(sprintf(
'Callee expected an EnumMap with value type %s, but got %s',
$keyType,
$this->keyType
));
}
if ($allowNullValues !== $this->allowNullValues) {
throw new ExpectationException(sprintf(
'Callee expected an EnumMap with nullable flag %s, but got %s',
($allowNullValues ? 'true' : 'false'),
($this->allowNullValues ? 'true' : 'false')
));
}
}
/**
* Returns the number of key-value mappings in this map.
*/
public function size() : int
{
return $this->size;
}
/**
* Returns true if this map maps one or more keys to the specified value.
*/
public function containsValue($value) : bool
{
return in_array($this->maskNull($value), $this->values, true);
}
/**
* Returns true if this map contains a mapping for the specified key.
*/
public function containsKey(AbstractEnum $key) : bool
{
$this->checkKeyType($key);
return null !== $this->values[$key->ordinal()];
}
/**
* Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
*
* More formally, if this map contains a mapping from a key to a value, then this method returns the value;
* otherwise it returns null (there can be at most one such mapping).
*
* A return value of null does not necessarily indicate that the map contains no mapping for the key; it's also
* possible that hte map explicitly maps the key to null. The {@see self::containsKey()} operation may be used to
* distinguish these two cases.
*
* @return mixed
*/
public function get(AbstractEnum $key)
{
$this->checkKeyType($key);
return $this->unmaskNull($this->values[$key->ordinal()]);
}
/**
* Associates the specified value with the specified key in this map.
*
* If the map previously contained a mapping for this key, the old value is replaced.
*
* @return mixed the previous value associated with the specified key, or null if there was no mapping for the key.
* (a null return can also indicate that the map previously associated null with the specified key.)
* @throws IllegalArgumentException when the passed values does not match the internal value type
*/
public function put(AbstractEnum $key, $value)
{
$this->checkKeyType($key);
if (! $this->isValidValue($value)) {
throw new IllegalArgumentException(sprintf('Value is not of type %s', $this->valueType));
}
$index = $key->ordinal();
$oldValue = $this->values[$index];
$this->values[$index] = $this->maskNull($value);
if (null === $oldValue) {
++$this->size;
}
return $this->unmaskNull($oldValue);
}
/**
* Removes the mapping for this key frm this map if present.
*
* @return mixed the previous value associated with the specified key, or null if there was no mapping for the key.
* (a null return can also indicate that the map previously associated null with the specified key.)
*/
public function remove(AbstractEnum $key)
{
$this->checkKeyType($key);
$index = $key->ordinal();
$oldValue = $this->values[$index];
$this->values[$index] = null;
if (null !== $oldValue) {
--$this->size;
}
return $this->unmaskNull($oldValue);
}
/**
* Removes all mappings from this map.
*/
public function clear() : void
{
$this->values = array_fill(0, count($this->keyUniverse), null);
$this->size = 0;
}
/**
* Compares the specified map with this map for quality.
*
* Returns true if the two maps represent the same mappings.
*/
public function equals(self $other) : bool
{
if ($this === $other) {
return true;
}
if ($this->size !== $other->size) {
return false;
}
return $this->values === $other->values;
}
/**
* Returns the values contained in this map.
*
* The array will contain the values in the order their corresponding keys appear in the map, which is their natural
* order (the order in which the num constants are declared).
*/
public function values() : array
{
return array_values(array_map(function ($value) {
return $this->unmaskNull($value);
}, array_filter($this->values, function ($value) : bool {
return null !== $value;
})));
}
public function serialize() : string
{
$values = [];
foreach ($this->values as $ordinal => $value) {
if (null === $value) {
continue;
}
$values[$ordinal] = $this->unmaskNull($value);
}
return serialize([
'keyType' => $this->keyType,
'valueType' => $this->valueType,
'allowNullValues' => $this->allowNullValues,
'values' => $values,
]);
}
public function unserialize($serialized) : void
{
$data = unserialize($serialized);
$this->__construct($data['keyType'], $data['valueType'], $data['allowNullValues']);
foreach ($this->keyUniverse as $key) {
if (array_key_exists($key->ordinal(), $data['values'])) {
$this->put($key, $data['values'][$key->ordinal()]);
}
}
}
public function getIterator() : Traversable
{
foreach ($this->keyUniverse as $key) {
if (null === $this->values[$key->ordinal()]) {
continue;
}
yield $key => $this->unmaskNull($this->values[$key->ordinal()]);
}
}
private function maskNull($value)
{
if (null === $value) {
return NullValue::instance();
}
return $value;
}
private function unmaskNull($value)
{
if ($value instanceof NullValue) {
return null;
}
return $value;
}
/**
* @throws IllegalArgumentException when the passed key does not match the internal key type
*/
private function checkKeyType(AbstractEnum $key) : void
{
if (get_class($key) !== $this->keyType) {
throw new IllegalArgumentException(sprintf(
'Object of type %s is not the same type as %s',
get_class($key),
$this->keyType
));
}
}
private function isValidValue($value) : bool
{
if (null === $value) {
if ($this->allowNullValues) {
return true;
}
return false;
}
switch ($this->valueType) {
case 'mixed':
return true;
case 'bool':
case 'boolean':
return is_bool($value);
case 'int':
case 'integer':
return is_int($value);
case 'float':
case 'double':
return is_float($value);
case 'string':
return is_string($value);
case 'object':
return is_object($value);
case 'array':
return is_array($value);
}
return $value instanceof $this->valueType;
}
}