1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace MabeEnum; |
||
6 | |||
7 | use ArrayAccess; |
||
8 | use Countable; |
||
9 | use InvalidArgumentException; |
||
10 | use Iterator; |
||
11 | use IteratorAggregate; |
||
12 | use UnexpectedValueException; |
||
13 | |||
14 | /** |
||
15 | * A map of enumerators and data values (EnumMap<T of Enum, mixed>). |
||
16 | * |
||
17 | * @template T of Enum |
||
18 | * @implements ArrayAccess<T, mixed> |
||
19 | * @implements IteratorAggregate<T, mixed> |
||
20 | * |
||
21 | * @copyright 2020, Marc Bennewitz |
||
22 | * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License |
||
23 | * @link http://github.com/marc-mabe/php-enum for the canonical source repository |
||
24 | */ |
||
25 | class EnumMap implements ArrayAccess, Countable, IteratorAggregate |
||
26 | { |
||
27 | /** |
||
28 | * The classname of the enumeration type |
||
29 | * @var class-string<T> |
||
30 | */ |
||
31 | private $enumeration; |
||
32 | |||
33 | /** |
||
34 | * Internal map of ordinal number and data value |
||
35 | * @var array<int, mixed> |
||
36 | */ |
||
37 | private $map = []; |
||
38 | |||
39 | /** |
||
40 | * Constructor |
||
41 | * @param class-string<T> $enumeration The classname of the enumeration type |
||
42 | * @param null|iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map Initialize map |
||
43 | * @throws InvalidArgumentException |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
44 | */ |
||
45 | 21 | public function __construct(string $enumeration, iterable $map = null) |
|
46 | { |
||
47 | 21 | if (!\is_subclass_of($enumeration, Enum::class)) { |
|
48 | 1 | throw new InvalidArgumentException(\sprintf( |
|
49 | 1 | '%s can handle subclasses of %s only', |
|
50 | 1 | __CLASS__, |
|
51 | 1 | Enum::class |
|
52 | )); |
||
53 | } |
||
54 | 20 | $this->enumeration = $enumeration; |
|
55 | |||
56 | 20 | if ($map) { |
|
57 | 3 | $this->addIterable($map); |
|
58 | } |
||
59 | 20 | } |
|
60 | |||
61 | /** |
||
62 | * Add virtual private property "__pairs" with a list of key-value-pairs |
||
63 | * to the result of var_dump. |
||
64 | * |
||
65 | * This helps debugging as internally the map is using the ordinal number. |
||
66 | * |
||
67 | * @return array<string, mixed> |
||
68 | */ |
||
69 | 1 | public function __debugInfo(): array { |
|
70 | 1 | $dbg = (array)$this; |
|
71 | 1 | $dbg["\0" . self::class . "\0__pairs"] = array_map(function ($k, $v) { |
|
72 | 1 | return [$k, $v]; |
|
73 | 1 | }, $this->getKeys(), $this->getValues()); |
|
74 | 1 | return $dbg; |
|
75 | } |
||
76 | |||
77 | /* write access (mutable) */ |
||
78 | |||
79 | /** |
||
80 | * Adds the given enumerator (object or value) mapping to the specified data value. |
||
81 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
82 | * @param mixed $value |
||
83 | * @throws InvalidArgumentException On an invalid given enumerator |
||
84 | * @see offsetSet() |
||
85 | */ |
||
86 | 13 | public function add($enumerator, $value): void |
|
87 | { |
||
88 | 13 | $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); |
|
89 | 12 | $this->map[$ord] = $value; |
|
90 | 12 | } |
|
91 | |||
92 | /** |
||
93 | * Adds the given iterable, mapping enumerators (objects or values) to data values. |
||
94 | * @param iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map |
||
95 | * @throws InvalidArgumentException On an invalid given enumerator |
||
96 | */ |
||
97 | 5 | public function addIterable(iterable $map): void |
|
98 | { |
||
99 | 5 | $innerMap = $this->map; |
|
100 | 5 | foreach ($map as $enumerator => $value) { |
|
101 | 5 | $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); |
|
102 | 5 | $innerMap[$ord] = $value; |
|
103 | } |
||
104 | 5 | $this->map = $innerMap; |
|
105 | 5 | } |
|
106 | |||
107 | /** |
||
108 | * Removes the given enumerator (object or value) mapping. |
||
109 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
110 | * @throws InvalidArgumentException On an invalid given enumerator |
||
111 | * @see offsetUnset() |
||
112 | */ |
||
113 | 5 | public function remove($enumerator): void |
|
114 | { |
||
115 | 5 | $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); |
|
116 | 5 | unset($this->map[$ord]); |
|
117 | 5 | } |
|
118 | |||
119 | /** |
||
120 | * Removes the given iterable enumerator (object or value) mappings. |
||
121 | * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators |
||
122 | * @throws InvalidArgumentException On an invalid given enumerator |
||
123 | */ |
||
124 | 3 | public function removeIterable(iterable $enumerators): void |
|
125 | { |
||
126 | 3 | $map = $this->map; |
|
127 | 3 | foreach ($enumerators as $enumerator) { |
|
128 | 3 | $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); |
|
129 | 3 | unset($map[$ord]); |
|
130 | } |
||
131 | |||
132 | 3 | $this->map = $map; |
|
133 | 3 | } |
|
134 | |||
135 | /* write access (immutable) */ |
||
136 | |||
137 | /** |
||
138 | * Creates a new map with the given enumerator (object or value) mapping to the specified data value added. |
||
139 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
140 | * @param mixed $value |
||
141 | * @return static |
||
142 | * @throws InvalidArgumentException On an invalid given enumerator |
||
143 | */ |
||
144 | 1 | public function with($enumerator, $value): self |
|
145 | { |
||
146 | 1 | $clone = clone $this; |
|
147 | 1 | $clone->add($enumerator, $value); |
|
148 | 1 | return $clone; |
|
149 | } |
||
150 | |||
151 | /** |
||
152 | * Creates a new map with the given iterable mapping enumerators (objects or values) to data values added. |
||
153 | * @param iterable<T|null|bool|int|float|string|array<mixed>, mixed> $map |
||
154 | * @return static |
||
155 | * @throws InvalidArgumentException On an invalid given enumerator |
||
156 | */ |
||
157 | 1 | public function withIterable(iterable $map): self |
|
158 | { |
||
159 | 1 | $clone = clone $this; |
|
160 | 1 | $clone->addIterable($map); |
|
161 | 1 | return $clone; |
|
162 | } |
||
163 | |||
164 | /** |
||
165 | * Create a new map with the given enumerator mapping removed. |
||
166 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
167 | * @return static |
||
168 | * @throws InvalidArgumentException On an invalid given enumerator |
||
169 | */ |
||
170 | 1 | public function without($enumerator): self |
|
171 | { |
||
172 | 1 | $clone = clone $this; |
|
173 | 1 | $clone->remove($enumerator); |
|
174 | 1 | return $clone; |
|
175 | } |
||
176 | |||
177 | /** |
||
178 | * Creates a new map with the given iterable enumerator (object or value) mappings removed. |
||
179 | * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators |
||
180 | * @return static |
||
181 | * @throws InvalidArgumentException On an invalid given enumerator |
||
182 | */ |
||
183 | 1 | public function withoutIterable(iterable $enumerators): self |
|
184 | { |
||
185 | 1 | $clone = clone $this; |
|
186 | 1 | $clone->removeIterable($enumerators); |
|
187 | 1 | return $clone; |
|
188 | } |
||
189 | |||
190 | /* read access */ |
||
191 | |||
192 | /** |
||
193 | * Get the classname of the enumeration type. |
||
194 | * @return class-string<T> |
||
195 | */ |
||
196 | 1 | public function getEnumeration(): string |
|
197 | { |
||
198 | 1 | return $this->enumeration; |
|
199 | } |
||
200 | |||
201 | /** |
||
202 | * Get the mapped data value of the given enumerator (object or value). |
||
203 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
204 | * @return mixed |
||
205 | * @throws InvalidArgumentException On an invalid given enumerator |
||
206 | * @throws UnexpectedValueException If the given enumerator does not exist in this map |
||
207 | * @see offsetGet() |
||
208 | */ |
||
209 | 10 | public function get($enumerator) |
|
210 | { |
||
211 | 10 | $enumerator = ($this->enumeration)::get($enumerator); |
|
212 | 10 | $ord = $enumerator->getOrdinal(); |
|
213 | 10 | if (!\array_key_exists($ord, $this->map)) { |
|
214 | 2 | throw new UnexpectedValueException(sprintf( |
|
215 | 2 | 'Enumerator %s could not be found', |
|
216 | 2 | \var_export($enumerator->getValue(), true) |
|
217 | )); |
||
218 | } |
||
219 | |||
220 | 8 | return $this->map[$ord]; |
|
221 | } |
||
222 | |||
223 | /** |
||
224 | * Get a list of enumerator keys. |
||
225 | * @return T[] |
||
226 | * |
||
227 | * @phpstan-return array<int, T> |
||
228 | * @psalm-return list<T> |
||
229 | */ |
||
230 | 8 | public function getKeys(): array |
|
231 | { |
||
232 | /** @var callable $byOrdinalFn */ |
||
233 | 8 | $byOrdinalFn = [$this->enumeration, 'byOrdinal']; |
|
234 | |||
235 | 8 | return \array_map($byOrdinalFn, \array_keys($this->map)); |
|
236 | } |
||
237 | |||
238 | /** |
||
239 | * Get a list of mapped data values. |
||
240 | * @return mixed[] |
||
241 | * |
||
242 | * @phpstan-return array<int, mixed> |
||
243 | * @psalm-return list<mixed> |
||
244 | */ |
||
245 | 8 | public function getValues(): array |
|
246 | { |
||
247 | 8 | return \array_values($this->map); |
|
248 | } |
||
249 | |||
250 | /** |
||
251 | * Search for the given data value. |
||
252 | * @param mixed $value |
||
253 | * @param bool $strict Use strict type comparison |
||
254 | * @return T|null The enumerator object of the first matching data value or NULL |
||
255 | */ |
||
256 | 2 | public function search($value, bool $strict = false) |
|
257 | { |
||
258 | /** @var false|int $ord */ |
||
259 | 2 | $ord = \array_search($value, $this->map, $strict); |
|
260 | 2 | if ($ord !== false) { |
|
261 | 2 | return ($this->enumeration)::byOrdinal($ord); |
|
262 | } |
||
263 | |||
264 | 2 | return null; |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * Test if the given enumerator key (object or value) exists. |
||
269 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
270 | * @return bool |
||
271 | * @see offsetExists() |
||
272 | */ |
||
273 | 8 | public function has($enumerator): bool |
|
274 | { |
||
275 | try { |
||
276 | 8 | $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); |
|
277 | 7 | return \array_key_exists($ord, $this->map); |
|
278 | 1 | } catch (InvalidArgumentException $e) { |
|
279 | // An invalid enumerator can't be contained in this map |
||
280 | 1 | return false; |
|
281 | } |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * Test if the given enumerator key (object or value) exists. |
||
286 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
287 | * @return bool |
||
288 | * @see offsetExists() |
||
289 | * @see has() |
||
290 | * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x |
||
291 | */ |
||
292 | 1 | public function contains($enumerator): bool |
|
293 | { |
||
294 | 1 | return $this->has($enumerator); |
|
295 | } |
||
296 | |||
297 | /* ArrayAccess */ |
||
298 | |||
299 | /** |
||
300 | * Test if the given enumerator key (object or value) exists and is not NULL |
||
301 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
302 | * @return bool |
||
303 | * @see contains() |
||
304 | */ |
||
305 | 5 | public function offsetExists($enumerator): bool |
|
306 | { |
||
307 | try { |
||
308 | 5 | return isset($this->map[($this->enumeration)::get($enumerator)->getOrdinal()]); |
|
309 | 1 | } catch (InvalidArgumentException $e) { |
|
310 | // An invalid enumerator can't be an offset of this map |
||
311 | 1 | return false; |
|
312 | } |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * Get the mapped data value of the given enumerator (object or value). |
||
317 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
318 | * @return mixed The mapped date value of the given enumerator or NULL |
||
319 | * @throws InvalidArgumentException On an invalid given enumerator |
||
320 | * @see get() |
||
321 | */ |
||
322 | 4 | public function offsetGet($enumerator) |
|
323 | { |
||
324 | try { |
||
325 | 4 | return $this->get($enumerator); |
|
326 | 1 | } catch (UnexpectedValueException $e) { |
|
327 | 1 | return null; |
|
328 | } |
||
329 | } |
||
330 | |||
331 | /** |
||
332 | * Adds the given enumerator (object or value) mapping to the specified data value. |
||
333 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
334 | * @param mixed $value |
||
335 | * @return void |
||
336 | * @throws InvalidArgumentException On an invalid given enumerator |
||
337 | * @see add() |
||
338 | */ |
||
339 | 7 | public function offsetSet($enumerator, $value = null): void |
|
340 | { |
||
341 | 7 | $this->add($enumerator, $value); |
|
342 | 6 | } |
|
343 | |||
344 | /** |
||
345 | * Removes the given enumerator (object or value) mapping. |
||
346 | * @param T|null|bool|int|float|string|array<mixed> $enumerator |
||
347 | * @return void |
||
348 | * @throws InvalidArgumentException On an invalid given enumerator |
||
349 | * @see remove() |
||
350 | */ |
||
351 | 2 | public function offsetUnset($enumerator): void |
|
352 | { |
||
353 | 2 | $this->remove($enumerator); |
|
354 | 2 | } |
|
355 | |||
356 | /* IteratorAggregate */ |
||
357 | |||
358 | /** |
||
359 | * Get a new Iterator. |
||
360 | * |
||
361 | * @return Iterator<T, mixed> Iterator<K extends Enum, V> |
||
362 | */ |
||
363 | 2 | public function getIterator(): Iterator |
|
364 | { |
||
365 | 2 | $map = $this->map; |
|
366 | 2 | foreach ($map as $ordinal => $value) { |
|
367 | 2 | yield ($this->enumeration)::byOrdinal($ordinal) => $value; |
|
368 | } |
||
369 | 2 | } |
|
370 | |||
371 | /* Countable */ |
||
372 | |||
373 | /** |
||
374 | * Count the number of elements |
||
375 | * |
||
376 | * @return int |
||
377 | */ |
||
378 | 3 | public function count(): int |
|
379 | { |
||
380 | 3 | return \count($this->map); |
|
381 | } |
||
382 | |||
383 | /** |
||
384 | * Tests if the map is empty |
||
385 | * |
||
386 | * @return bool |
||
387 | */ |
||
388 | 1 | public function isEmpty(): bool |
|
389 | { |
||
390 | 1 | return empty($this->map); |
|
391 | } |
||
392 | } |
||
393 |