*/ private $value; /** * The ordinal number of the enumerator * * @var null|int */ private $ordinal; /** * A map of enumerator names and values by enumeration class * * @var array, array>> */ private static $constants = []; /** * A List of available enumerator names by enumeration class * * @var array, string[]> */ private static $names = []; /** * A map of enumerator names and instances by enumeration class * * @var array, array> */ private static $instances = []; /** * Constructor * * @param null|bool|int|float|string|array $value The value of the enumerator * @param int|null $ordinal The ordinal number of the enumerator */ final private function __construct($value, $ordinal = null) { $this->value = $value; $this->ordinal = $ordinal; } /** * Get the name of the enumerator * * @return string * @see getName() */ public function __toString(): string { return $this->getName(); } /** * @throws LogicException Enums are not cloneable * because instances are implemented as singletons */ final public function __clone() { throw new LogicException('Enums are not cloneable'); } /** * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation * * @psalm-return never-return */ final public function __sleep() { throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); } /** * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation * * @psalm-return never-return */ final public function __wakeup() { throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); } /** * Get the value of the enumerator * * @return null|bool|int|float|string|array */ final public function getValue() { return $this->value; } /** * Get the name of the enumerator * * @return string * * @phpstan-return string * @psalm-return non-empty-string */ final public function getName() { return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()]; } /** * Get the ordinal number of the enumerator * * @return int */ final public function getOrdinal() { if ($this->ordinal === null) { $ordinal = 0; $value = $this->value; $constants = self::$constants[static::class] ?? static::getConstants(); foreach ($constants as $constValue) { if ($value === $constValue) { break; } ++$ordinal; } $this->ordinal = $ordinal; } return $this->ordinal; } /** * Compare this enumerator against another and check if it's the same. * * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value * @return bool */ final public function is($enumerator) { return $this === $enumerator || $this->value === $enumerator // The following additional conditions are required only because of the issue of serializable singletons || ($enumerator instanceof static && \get_class($enumerator) === static::class && $enumerator->value === $this->value ); } /** * Get an enumerator instance of the given enumerator value or instance * * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value * @return static * @throws InvalidArgumentException On an unknown or invalid value * @throws LogicException On ambiguous constant values * * @psalm-pure */ final public static function get($enumerator) { if ($enumerator instanceof static) { if (\get_class($enumerator) !== static::class) { throw new InvalidArgumentException(sprintf( 'Invalid value of type %s for enumeration %s', \get_class($enumerator), static::class )); } return $enumerator; } return static::byValue($enumerator); } /** * Get an enumerator instance by the given value * * @param null|bool|int|float|string|array $value Enumerator value * @return static * @throws InvalidArgumentException On an unknown or invalid value * @throws LogicException On ambiguous constant values * * @psalm-pure */ final public static function byValue($value) { /** @var mixed $value */ $constants = self::$constants[static::class] ?? static::getConstants(); $name = \array_search($value, $constants, true); if ($name === false) { throw new InvalidArgumentException(sprintf( 'Unknown value %s for enumeration %s', \is_scalar($value) ? \var_export($value, true) : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)), static::class )); } /** @var static $instance */ $instance = self::$instances[static::class][$name] ?? self::$instances[static::class][$name] = new static($constants[$name]); return $instance; } /** * Get an enumerator instance by the given name * * @param string $name The name of the enumerator * @return static * @throws InvalidArgumentException On an invalid or unknown name * @throws LogicException On ambiguous values * * @psalm-pure */ final public static function byName(string $name) { if (isset(self::$instances[static::class][$name])) { /** @var static $instance */ $instance = self::$instances[static::class][$name]; return $instance; } $const = static::class . "::{$name}"; if (!\defined($const)) { throw new InvalidArgumentException("{$const} not defined"); } assert( self::noAmbiguousValues(static::getConstants()), 'Ambiguous enumerator values detected for ' . static::class ); /** @var array|bool|float|int|string|null $value */ $value = \constant($const); return self::$instances[static::class][$name] = new static($value); } /** * Get an enumeration instance by the given ordinal number * * @param int $ordinal The ordinal number of the enumerator * @return static * @throws InvalidArgumentException On an invalid ordinal number * @throws LogicException On ambiguous values * * @psalm-pure */ final public static function byOrdinal(int $ordinal) { $constants = self::$constants[static::class] ?? static::getConstants(); if (!isset(self::$names[static::class][$ordinal])) { throw new InvalidArgumentException(\sprintf( 'Invalid ordinal number %s, must between 0 and %s', $ordinal, \count(self::$names[static::class]) - 1 )); } $name = self::$names[static::class][$ordinal]; /** @var static $instance */ $instance = self::$instances[static::class][$name] ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal); return $instance; } /** * Get a list of enumerator instances ordered by ordinal number * * @return static[] * * @phpstan-return array * @psalm-return list * @psalm-pure */ final public static function getEnumerators() { if (!isset(self::$names[static::class])) { static::getConstants(); } /** @var callable $byNameFn */ $byNameFn = [static::class, 'byName']; return \array_map($byNameFn, self::$names[static::class]); } /** * Get a list of enumerator values ordered by ordinal number * * @return (null|bool|int|float|string|array)[] * * @phpstan-return array> * @psalm-return list * @psalm-pure */ final public static function getValues() { return \array_values(self::$constants[static::class] ?? static::getConstants()); } /** * Get a list of enumerator names ordered by ordinal number * * @return string[] * * @phpstan-return array * @psalm-return list * @psalm-pure */ final public static function getNames() { if (!isset(self::$names[static::class])) { static::getConstants(); } return self::$names[static::class]; } /** * Get a list of enumerator ordinal numbers * * @return int[] * * @phpstan-return array * @psalm-return list * @psalm-pure */ final public static function getOrdinals() { $count = \count(self::$constants[static::class] ?? static::getConstants()); return $count ? \range(0, $count - 1) : []; } /** * Get all available constants of the called class * * @return (null|bool|int|float|string|array)[] * @throws LogicException On ambiguous constant values * * @phpstan-return array> * @psalm-return array * @psalm-pure */ final public static function getConstants() { if (isset(self::$constants[static::class])) { return self::$constants[static::class]; } $reflection = new ReflectionClass(static::class); $constants = []; do { $scopeConstants = []; // Enumerators must be defined as public class constants foreach ($reflection->getReflectionConstants() as $reflConstant) { if ($reflConstant->isPublic()) { $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue(); } } $constants = $scopeConstants + $constants; } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__); /** @var array> $constants */ assert( self::noAmbiguousValues($constants), 'Ambiguous enumerator values detected for ' . static::class ); self::$names[static::class] = \array_keys($constants); return self::$constants[static::class] = $constants; } /** * Test that the given constants does not contain ambiguous values * @param array> $constants * @return bool */ private static function noAmbiguousValues($constants) { foreach ($constants as $value) { $names = \array_keys($constants, $value, true); if (\count($names) > 1) { return false; } } return true; } /** * Test if the given enumerator is part of this enumeration * * @param static|null|bool|int|float|string|array $enumerator * @return bool * * @psalm-pure */ final public static function has($enumerator) { if ($enumerator instanceof static) { return \get_class($enumerator) === static::class; } return static::hasValue($enumerator); } /** * Test if the given enumerator value is part of this enumeration * * @param null|bool|int|float|string|array $value * @return bool * * @psalm-pure */ final public static function hasValue($value) { return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true); } /** * Test if the given enumerator name is part of this enumeration * * @param string $name * @return bool * * @psalm-pure */ final public static function hasName(string $name) { return \defined("static::{$name}"); } /** * Get an enumerator instance by the given name. * * This will be called automatically on calling a method * with the same name of a defined enumerator. * * @param string $method The name of the enumerator (called as method) * @param array $args There should be no arguments * @return static * @throws InvalidArgumentException On an invalid or unknown name * @throws LogicException On ambiguous constant values * * @psalm-pure */ final public static function __callStatic(string $method, array $args) { return static::byName($method); } }