marc-mabe /
php-enum
| 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 |
||
|
0 ignored issues
–
show
Coding Style
introduced
by
Loading history...
|
|||
| 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; |
||
|
0 ignored issues
–
show
|
|||
| 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
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 82 | * because instances are implemented as singletons |
||
| 83 | */ |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 91 | * because instances are implemented as singletons |
||
| 92 | * |
||
| 93 | * @psalm-return never-return |
||
| 94 | */ |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 102 | * because instances are implemented as singletons |
||
| 103 | * |
||
| 104 | * @psalm-return never-return |
||
| 105 | */ |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 181 | * @throws LogicException On ambiguous constant values |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 208 | * @throws LogicException On ambiguous constant values |
||
|
0 ignored issues
–
show
|
|||
| 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]); |
|
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 242 | * @throws LogicException On ambiguous values |
||
|
0 ignored issues
–
show
|
|||
| 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}"; |
|
|
0 ignored issues
–
show
As per coding-style, please use concatenation or
sprintf for the variable $name instead of interpolation.
It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings. // Instead of
$x = "foo $bar $baz";
// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
|
|||
| 255 | 20 | if (!\defined($const)) { |
|
| 256 | 1 | throw new InvalidArgumentException("{$const} not defined"); |
|
|
0 ignored issues
–
show
As per coding-style, please use concatenation or
sprintf for the variable $const instead of interpolation.
It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings. // Instead of
$x = "foo $bar $baz";
// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 273 | * @throws LogicException On ambiguous values |
||
|
0 ignored issues
–
show
|
|||
| 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); |
|
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 455 | * @return bool |
||
| 456 | * |
||
| 457 | * @psalm-pure |
||
| 458 | */ |
||
| 459 | 1 | final public static function hasName(string $name) |
|
| 460 | { |
||
| 461 | 1 | return \defined("static::{$name}"); |
|
|
0 ignored issues
–
show
As per coding-style, please use concatenation or
sprintf for the variable $name instead of interpolation.
It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings. // Instead of
$x = "foo $bar $baz";
// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
|
|||
| 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 |
||
|
0 ignored issues
–
show
|
|||
| 474 | * @throws LogicException On ambiguous constant values |
||
|
0 ignored issues
–
show
|
|||
| 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 |