1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace MabeEnum; |
||
6 | |||
7 | use ReflectionClass; |
||
8 | use InvalidArgumentException; |
||
9 | use LogicException; |
||
10 | |||
11 | /** |
||
12 | * Abstract base enumeration class. |
||
13 | * |
||
14 | * @copyright 2020, Marc Bennewitz |
||
15 | * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License |
||
16 | * @link http://github.com/marc-mabe/php-enum for the canonical source repository |
||
17 | * |
||
18 | * @psalm-immutable |
||
19 | */ |
||
20 | abstract class Enum |
||
21 | { |
||
22 | /** |
||
23 | * The selected enumerator value |
||
24 | * |
||
25 | * @var null|bool|int|float|string|array<mixed> |
||
26 | */ |
||
27 | private $value; |
||
28 | |||
29 | /** |
||
30 | * The ordinal number of the enumerator |
||
31 | * |
||
32 | * @var null|int |
||
33 | */ |
||
34 | private $ordinal; |
||
35 | |||
36 | /** |
||
37 | * A map of enumerator names and values by enumeration class |
||
38 | * |
||
39 | * @var array<class-string<Enum>, array<string, null|bool|int|float|string|array<mixed>>> |
||
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
40 | */ |
||
41 | private static $constants = []; |
||
42 | |||
43 | /** |
||
44 | * A List of available enumerator names by enumeration class |
||
45 | * |
||
46 | * @var array<class-string<Enum>, string[]> |
||
0 ignored issues
–
show
|
|||
47 | */ |
||
48 | private static $names = []; |
||
49 | |||
50 | /** |
||
51 | * A map of enumerator names and instances by enumeration class |
||
52 | * |
||
53 | * @var array<class-string<Enum>, array<string, Enum>> |
||
0 ignored issues
–
show
|
|||
54 | */ |
||
55 | private static $instances = []; |
||
56 | |||
57 | /** |
||
58 | * Constructor |
||
59 | * |
||
60 | * @param null|bool|int|float|string|array<mixed> $value The value of the enumerator |
||
61 | * @param int|null $ordinal The ordinal number of the enumerator |
||
62 | */ |
||
63 | 46 | final private function __construct($value, $ordinal = null) |
|
64 | { |
||
65 | 46 | $this->value = $value; |
|
66 | 46 | $this->ordinal = $ordinal; |
|
67 | 46 | } |
|
68 | |||
69 | /** |
||
70 | * Get the name of the enumerator |
||
71 | * |
||
72 | * @return string |
||
73 | * @see getName() |
||
74 | */ |
||
75 | 1 | public function __toString(): string |
|
76 | { |
||
77 | 1 | return $this->getName(); |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * @throws LogicException Enums are not cloneable |
||
82 | * because instances are implemented as singletons |
||
83 | */ |
||
84 | 1 | final protected function __clone() |
|
85 | { |
||
86 | 1 | throw new LogicException('Enums are not cloneable'); |
|
87 | } |
||
88 | |||
89 | /** |
||
90 | * @throws LogicException Enums are not serializable |
||
91 | * because instances are implemented as singletons |
||
92 | * |
||
93 | * @psalm-return never-return |
||
94 | */ |
||
95 | 1 | final public function __sleep() |
|
96 | { |
||
97 | 1 | throw new LogicException('Enums are not serializable'); |
|
98 | } |
||
99 | |||
100 | /** |
||
101 | * @throws LogicException Enums are not serializable |
||
102 | * because instances are implemented as singletons |
||
103 | * |
||
104 | * @psalm-return never-return |
||
105 | */ |
||
106 | 1 | final public function __wakeup() |
|
107 | { |
||
108 | 1 | throw new LogicException('Enums are not serializable'); |
|
109 | } |
||
110 | |||
111 | /** |
||
112 | * Get the value of the enumerator |
||
113 | * |
||
114 | * @return null|bool|int|float|string|array<mixed> |
||
115 | */ |
||
116 | 33 | final public function getValue() |
|
117 | { |
||
118 | 33 | return $this->value; |
|
119 | } |
||
120 | |||
121 | /** |
||
122 | * Get the name of the enumerator |
||
123 | * |
||
124 | * @return string |
||
125 | * |
||
126 | * @phpstan-return string |
||
127 | * @psalm-return non-empty-string |
||
128 | */ |
||
129 | 8 | final public function getName() |
|
130 | { |
||
131 | 8 | return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()]; |
|
132 | } |
||
133 | |||
134 | /** |
||
135 | * Get the ordinal number of the enumerator |
||
136 | * |
||
137 | * @return int |
||
138 | */ |
||
139 | 111 | final public function getOrdinal() |
|
140 | { |
||
141 | 111 | if ($this->ordinal === null) { |
|
142 | 23 | $ordinal = 0; |
|
143 | 23 | $value = $this->value; |
|
144 | 23 | $constants = self::$constants[static::class] ?? static::getConstants(); |
|
145 | 23 | foreach ($constants as $constValue) { |
|
146 | 23 | if ($value === $constValue) { |
|
147 | 23 | break; |
|
148 | } |
||
149 | 18 | ++$ordinal; |
|
150 | } |
||
151 | |||
152 | 23 | $this->ordinal = $ordinal; |
|
153 | } |
||
154 | |||
155 | 111 | return $this->ordinal; |
|
156 | } |
||
157 | |||
158 | /** |
||
159 | * Compare this enumerator against another and check if it's the same. |
||
160 | * |
||
161 | * @param static|null|bool|int|float|string|array<mixed> $enumerator An enumerator object or value |
||
162 | * @return bool |
||
163 | */ |
||
164 | 2 | final public function is($enumerator) |
|
165 | { |
||
166 | 2 | return $this === $enumerator || $this->value === $enumerator |
|
167 | |||
168 | // The following additional conditions are required only because of the issue of serializable singletons |
||
169 | 2 | || ($enumerator instanceof static |
|
170 | 2 | && \get_class($enumerator) === static::class |
|
171 | 2 | && $enumerator->value === $this->value |
|
172 | ); |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Get an enumerator instance of the given enumerator value or instance |
||
177 | * |
||
178 | * @param static|null|bool|int|float|string|array<mixed> $enumerator An enumerator object or value |
||
179 | * @return static |
||
180 | * @throws InvalidArgumentException On an unknown or invalid value |
||
181 | * @throws LogicException On ambiguous constant values |
||
182 | * |
||
183 | * @psalm-pure |
||
184 | */ |
||
185 | 122 | final public static function get($enumerator) |
|
186 | { |
||
187 | 122 | if ($enumerator instanceof static) { |
|
188 | 41 | if (\get_class($enumerator) !== static::class) { |
|
189 | 4 | throw new InvalidArgumentException(sprintf( |
|
190 | 4 | 'Invalid value of type %s for enumeration %s', |
|
191 | 4 | \get_class($enumerator), |
|
192 | 4 | static::class |
|
193 | )); |
||
194 | } |
||
195 | |||
196 | 37 | return $enumerator; |
|
197 | } |
||
198 | |||
199 | 93 | return static::byValue($enumerator); |
|
200 | } |
||
201 | |||
202 | /** |
||
203 | * Get an enumerator instance by the given value |
||
204 | * |
||
205 | * @param null|bool|int|float|string|array<mixed> $value Enumerator value |
||
206 | * @return static |
||
207 | * @throws InvalidArgumentException On an unknown or invalid value |
||
208 | * @throws LogicException On ambiguous constant values |
||
209 | * |
||
210 | * @psalm-pure |
||
211 | */ |
||
212 | 93 | final public static function byValue($value) |
|
213 | { |
||
214 | /** @var mixed $value */ |
||
215 | |||
216 | 93 | $constants = self::$constants[static::class] ?? static::getConstants(); |
|
217 | |||
218 | 91 | $name = \array_search($value, $constants, true); |
|
219 | 91 | if ($name === false) { |
|
220 | 9 | throw new InvalidArgumentException(sprintf( |
|
221 | 9 | 'Unknown value %s for enumeration %s', |
|
222 | 9 | \is_scalar($value) |
|
223 | 7 | ? \var_export($value, true) |
|
224 | 9 | : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)), |
|
225 | 9 | static::class |
|
226 | )); |
||
227 | } |
||
228 | |||
229 | /** @var static $instance */ |
||
230 | 84 | $instance = self::$instances[static::class][$name] |
|
231 | 84 | ?? self::$instances[static::class][$name] = new static($constants[$name]); |
|
232 | |||
233 | 84 | return $instance; |
|
234 | } |
||
235 | |||
236 | /** |
||
237 | * Get an enumerator instance by the given name |
||
238 | * |
||
239 | * @param string $name The name of the enumerator |
||
240 | * @return static |
||
241 | * @throws InvalidArgumentException On an invalid or unknown name |
||
242 | * @throws LogicException On ambiguous values |
||
243 | * |
||
244 | * @psalm-pure |
||
245 | */ |
||
246 | 61 | final public static function byName(string $name) |
|
247 | { |
||
248 | 61 | if (isset(self::$instances[static::class][$name])) { |
|
249 | /** @var static $instance */ |
||
250 | 46 | $instance = self::$instances[static::class][$name]; |
|
251 | 46 | return $instance; |
|
252 | } |
||
253 | |||
254 | 20 | $const = static::class . "::{$name}"; |
|
255 | 20 | if (!\defined($const)) { |
|
256 | 1 | throw new InvalidArgumentException("{$const} not defined"); |
|
257 | } |
||
258 | |||
259 | 19 | assert( |
|
260 | 19 | self::noAmbiguousValues(static::getConstants()), |
|
261 | 18 | 'Ambiguous enumerator values detected for ' . static::class |
|
262 | ); |
||
263 | |||
264 | 18 | return self::$instances[static::class][$name] = new static(\constant($const)); |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * Get an enumeration instance by the given ordinal number |
||
269 | * |
||
270 | * @param int $ordinal The ordinal number of the enumerator |
||
271 | * @return static |
||
272 | * @throws InvalidArgumentException On an invalid ordinal number |
||
273 | * @throws LogicException On ambiguous values |
||
274 | * |
||
275 | * @psalm-pure |
||
276 | */ |
||
277 | 41 | final public static function byOrdinal(int $ordinal) |
|
278 | { |
||
279 | 41 | $constants = self::$constants[static::class] ?? static::getConstants(); |
|
280 | |||
281 | 41 | if (!isset(self::$names[static::class][$ordinal])) { |
|
282 | 1 | throw new InvalidArgumentException(\sprintf( |
|
283 | 1 | 'Invalid ordinal number %s, must between 0 and %s', |
|
284 | 1 | $ordinal, |
|
285 | 1 | \count(self::$names[static::class]) - 1 |
|
286 | )); |
||
287 | } |
||
288 | |||
289 | 40 | $name = self::$names[static::class][$ordinal]; |
|
290 | |||
291 | /** @var static $instance */ |
||
292 | 40 | $instance = self::$instances[static::class][$name] |
|
293 | 40 | ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal); |
|
294 | |||
295 | 40 | return $instance; |
|
296 | } |
||
297 | |||
298 | /** |
||
299 | * Get a list of enumerator instances ordered by ordinal number |
||
300 | * |
||
301 | * @return static[] |
||
302 | * |
||
303 | * @phpstan-return array<int, static> |
||
304 | * @psalm-return list<static> |
||
305 | * @psalm-pure |
||
306 | */ |
||
307 | 19 | final public static function getEnumerators() |
|
308 | { |
||
309 | 19 | if (!isset(self::$names[static::class])) { |
|
310 | 1 | static::getConstants(); |
|
311 | } |
||
312 | |||
313 | /** @var callable $byNameFn */ |
||
314 | 19 | $byNameFn = [static::class, 'byName']; |
|
315 | 19 | return \array_map($byNameFn, self::$names[static::class]); |
|
316 | } |
||
317 | |||
318 | /** |
||
319 | * Get a list of enumerator values ordered by ordinal number |
||
320 | * |
||
321 | * @return (null|bool|int|float|string|array)[] |
||
322 | * |
||
323 | * @phpstan-return array<int, null|bool|int|float|string|array> |
||
324 | * @psalm-return list<null|bool|int|float|string|array> |
||
325 | * @psalm-pure |
||
326 | */ |
||
327 | 8 | final public static function getValues() |
|
328 | { |
||
329 | 8 | return \array_values(self::$constants[static::class] ?? static::getConstants()); |
|
330 | } |
||
331 | |||
332 | /** |
||
333 | * Get a list of enumerator names ordered by ordinal number |
||
334 | * |
||
335 | * @return string[] |
||
336 | * |
||
337 | * @phpstan-return array<int, string> |
||
338 | * @psalm-return list<non-empty-string> |
||
339 | * @psalm-pure |
||
340 | */ |
||
341 | 3 | final public static function getNames() |
|
342 | { |
||
343 | 3 | if (!isset(self::$names[static::class])) { |
|
344 | 1 | static::getConstants(); |
|
345 | } |
||
346 | 3 | return self::$names[static::class]; |
|
347 | } |
||
348 | |||
349 | /** |
||
350 | * Get a list of enumerator ordinal numbers |
||
351 | * |
||
352 | * @return int[] |
||
353 | * |
||
354 | * @phpstan-return array<int, int> |
||
355 | * @psalm-return list<int> |
||
356 | * @psalm-pure |
||
357 | */ |
||
358 | 1 | final public static function getOrdinals() |
|
359 | { |
||
360 | 1 | $count = \count(self::$constants[static::class] ?? static::getConstants()); |
|
361 | 1 | return $count ? \range(0, $count - 1) : []; |
|
362 | } |
||
363 | |||
364 | /** |
||
365 | * Get all available constants of the called class |
||
366 | * |
||
367 | * @return (null|bool|int|float|string|array)[] |
||
368 | * @throws LogicException On ambiguous constant values |
||
369 | * |
||
370 | * @phpstan-return array<string, null|bool|int|float|string|array> |
||
371 | * @psalm-return array<non-empty-string, null|bool|int|float|string|array> |
||
372 | * @psalm-pure |
||
373 | */ |
||
374 | 154 | final public static function getConstants() |
|
375 | { |
||
376 | 154 | if (isset(self::$constants[static::class])) { |
|
377 | 118 | return self::$constants[static::class]; |
|
378 | } |
||
379 | |||
380 | 51 | $reflection = new ReflectionClass(static::class); |
|
381 | 51 | $constants = []; |
|
382 | |||
383 | do { |
||
384 | 51 | $scopeConstants = []; |
|
385 | // Enumerators must be defined as public class constants |
||
386 | 51 | foreach ($reflection->getReflectionConstants() as $reflConstant) { |
|
387 | 50 | if ($reflConstant->isPublic()) { |
|
388 | 50 | $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue(); |
|
389 | } |
||
390 | } |
||
391 | |||
392 | 51 | $constants = $scopeConstants + $constants; |
|
393 | 51 | } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__); |
|
394 | |||
395 | 51 | assert( |
|
396 | 51 | self::noAmbiguousValues($constants), |
|
397 | 51 | 'Ambiguous enumerator values detected for ' . static::class |
|
398 | ); |
||
399 | |||
400 | 48 | self::$names[static::class] = \array_keys($constants); |
|
401 | 48 | return self::$constants[static::class] = $constants; |
|
402 | } |
||
403 | |||
404 | /** |
||
405 | * Test that the given constants does not contain ambiguous values |
||
406 | * @param array<string, null|bool|int|float|string|array<mixed>> $constants |
||
407 | * @return bool |
||
408 | */ |
||
409 | 54 | private static function noAmbiguousValues($constants) |
|
410 | { |
||
411 | 54 | foreach ($constants as $value) { |
|
412 | 53 | $names = \array_keys($constants, $value, true); |
|
413 | 53 | if (\count($names) > 1) { |
|
414 | 5 | return false; |
|
415 | } |
||
416 | } |
||
417 | |||
418 | 49 | return true; |
|
419 | } |
||
420 | |||
421 | /** |
||
422 | * Test if the given enumerator is part of this enumeration |
||
423 | * |
||
424 | * @param static|null|bool|int|float|string|array<mixed> $enumerator |
||
425 | * @return bool |
||
426 | * |
||
427 | * @psalm-pure |
||
428 | */ |
||
429 | 1 | final public static function has($enumerator) |
|
430 | { |
||
431 | 1 | if ($enumerator instanceof static) { |
|
432 | 1 | return \get_class($enumerator) === static::class; |
|
433 | } |
||
434 | |||
435 | 1 | return static::hasValue($enumerator); |
|
436 | } |
||
437 | |||
438 | /** |
||
439 | * Test if the given enumerator value is part of this enumeration |
||
440 | * |
||
441 | * @param null|bool|int|float|string|array<mixed> $value |
||
442 | * @return bool |
||
443 | * |
||
444 | * @psalm-pure |
||
445 | */ |
||
446 | 2 | final public static function hasValue($value) |
|
447 | { |
||
448 | 2 | return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true); |
|
449 | } |
||
450 | |||
451 | /** |
||
452 | * Test if the given enumerator name is part of this enumeration |
||
453 | * |
||
454 | * @param string $name |
||
455 | * @return bool |
||
456 | * |
||
457 | * @psalm-pure |
||
458 | */ |
||
459 | 1 | final public static function hasName(string $name) |
|
460 | { |
||
461 | 1 | return \defined("static::{$name}"); |
|
462 | } |
||
463 | |||
464 | /** |
||
465 | * Get an enumerator instance by the given name. |
||
466 | * |
||
467 | * This will be called automatically on calling a method |
||
468 | * with the same name of a defined enumerator. |
||
469 | * |
||
470 | * @param string $method The name of the enumerator (called as method) |
||
471 | * @param array<mixed> $args There should be no arguments |
||
472 | * @return static |
||
473 | * @throws InvalidArgumentException On an invalid or unknown name |
||
474 | * @throws LogicException On ambiguous constant values |
||
475 | * |
||
476 | * @psalm-pure |
||
477 | */ |
||
478 | 39 | final public static function __callStatic(string $method, array $args) |
|
479 | { |
||
480 | 39 | return static::byName($method); |
|
481 | } |
||
482 | } |
||
483 |