diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6632b..c39d9fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `cases` method from [Basic enumerations] - Add `from` method from [Backed enumerations] - Add `tryFrom` method from [Backed enumerations] +- Add [SplEnumerable] in order to render [SplEnum] more generic and usable with [SplReflectionEnum] - Add [SplUnitEnum] and abstract class [SplEnumUnit] in order to emulate [Basic enumerations] (PHP Pure enum) and [UnitEnum] - Add [SplBackedEnum] and abstract class [SplEnumBacked] in order to emulate [Backed enumerations] and [BackedEnum] - Add [SplIntEnum] @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -- [SplReflectionEnum] in order to simulate [ReflectionEnum] -- [SplReflectionEnumBackedCase] in order to simulate [ReflectionEnumBackedCase] -- [SplReflectionEnumUnitCase] in order to simulate [ReflectionEnumUnitCase] +-- [SplReflectionNamedType] in order to simulate [ReflectionNamedType] - Add [SplIntEnum] in order to simulate [Backed enumerations] typed as `int` - Add [SplStringEnum] in order to simulate [Backed enumerations] typed as `string` - Add `stubs` in order to use classes at root namespace @@ -158,6 +160,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [`SplEnum`]: /assets/documentation/classes/SplEnum.md [`SplBool`]: /assets/documentation/classes/SplBool.md [`SplString`]: /assets/documentation/classes/SplString.md +[`SplEnumerable`]: /assets/documentation/classes/SplEnumerable.md [`SplUnitEnum`]: /assets/documentation/classes/SplUnitEnum.md [`SplBackedEnum`]: /assets/documentation/classes/SplBackedEnum.md [`SplEnumUnit`]: /assets/documentation/classes/SplEnumUnit.md @@ -170,6 +173,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [`SplReflectionEnumUnitCase`]: /assets/documentation/classes/Reflection/SplReflectionEnumUnitCase.md [`SplReflectionEnumHelper`]: /assets/documentation/classes/Reflection/SplReflectionEnumHelper.md [`SplReflectionEnumProxy`]: /assets/documentation/classes/Reflection/SplReflectionEnumProxy.md +[`SplReflectionNamedType`]: /assets/documentation/classes/Reflection/SplReflectionNamedType.md [`Tools`]: /assets/documentation/classes/Util/Tools.md [SplType::__construct]: /assets/documentation/classes/SplType.construct.md#SplType::__construct [Tools::isSplEnumExists]: /assets/documentation/classes/Util/Tools.isSplEnumExist.md#Tools::isSplEnumExists diff --git a/README.md b/README.md index 09f3a7c..39ff920 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ This library is released under the [MIT license]. [`SplEnum`]: /assets/documentation/classes/SplEnum.md [`SplBool`]: /assets/documentation/classes/SplBool.md [`SplString`]: /assets/documentation/classes/SplString.md +[`SplEnumerable`]: /assets/documentation/classes/SplEnumerable.md [`SplUnitEnum`]: /assets/documentation/classes/SplUnitEnum.md [`SplBackedEnum`]: /assets/documentation/classes/SplBackedEnum.md [Lexique]: /assets/documentation/Lexique.md diff --git a/Reflection/SplReflectionEnum.php b/Reflection/SplReflectionEnum.php index 13d9481..e434cf0 100644 --- a/Reflection/SplReflectionEnum.php +++ b/Reflection/SplReflectionEnum.php @@ -13,13 +13,15 @@ namespace Ducks\Component\SplTypes\Reflection; -use Ducks\Component\SplTypes\SplUnitEnum; +use Ducks\Component\SplTypes\SplEnumerable; /** * The ReflectionEnum class reports information about an Enum. * - * @template T of SplUnitEnum - * @extends \ReflectionClass<\Ducks\Component\SplTypes\SplUnitEnum> + * @template T of SplEnumerable + * @extends \ReflectionClass<\Ducks\Component\SplTypes\SplEnumerable> + * + * @psalm-api * * @link https://php.net/manual/en/class.reflectionenum.php */ @@ -28,7 +30,9 @@ class SplReflectionEnum extends \ReflectionClass /** * Array of proxy * - * @var array + * @var SplReflectionEnumProxy[] + * + * @phpstan-var array */ private static array $instances = []; @@ -53,16 +57,18 @@ private function getProxy(): SplReflectionEnumProxy * * @param object|string $objectOrClass * - * @throws \ReflectionException if objectOrClass is not a SplUnitEnum + * @throws \ReflectionException if objectOrClass is not a SplEnumerable + * + * @phpstan-param T|class-string $objectOrClass * - * @phpstan-param SplUnitEnum|class-string $objectOrClass + * @psalm-pure * * @link https://www.php.net/manual/en/reflectionenum.construct.php */ public function __construct($objectOrClass) { // Fast check - if (!\is_a($objectOrClass, SplUnitEnum::class, true)) { + if (!\is_a($objectOrClass, SplEnumerable::class, true)) { // @phpstan-ignore ternary.elseUnreachable $classname = \is_object($objectOrClass) ? \get_class($objectOrClass) : $objectOrClass; throw new \ReflectionException("Class \"$classname\" is not an enum"); @@ -102,7 +108,7 @@ public function getCase(string $name): SplReflectionEnumUnitCase /** * Returns a list of all cases on an Enum * - * @return array + * @return (SplReflectionEnumUnitCase|SplReflectionEnumBackedCase)[] * * @phpstan-return list * diff --git a/Reflection/SplReflectionEnumBackedCase.php b/Reflection/SplReflectionEnumBackedCase.php index 981163b..5b4b1f3 100644 --- a/Reflection/SplReflectionEnumBackedCase.php +++ b/Reflection/SplReflectionEnumBackedCase.php @@ -15,6 +15,13 @@ use Ducks\Component\SplTypes\SplUnitEnum; +/** + * The SplReflectionEnumBackedCase class reports info about an SplEnum backed case, which has no scalar equivalent. + * + * @psalm-api + * @psalm-immutable + * @psalm-suppress PropertyNotSetInConstructor + */ class SplReflectionEnumBackedCase extends SplReflectionEnumUnitCase { /** @@ -25,6 +32,13 @@ class SplReflectionEnumBackedCase extends SplReflectionEnumUnitCase * * @throws \ReflectionException if $class is not a \ReflectionEnumBackedCase * + * @phpcs:ignore Generic.Files.LineLength.TooLong + * @phpstan-param \Ducks\Component\SplTypes\SplEnumerable|class-string<\Ducks\Component\SplTypes\SplEnumerable> $class An enum instance or a name. + * @phpstan-param string $constant An enum constant name. + * + * @psalm-suppress UninitializedProperty + * @psalm-suppress ImpureMethodCall + * * @link https://www.php.net/manual/en/reflectionenumbackedcase.construct.php */ public function __construct($class, string $constant) @@ -32,6 +46,7 @@ public function __construct($class, string $constant) parent::__construct($class, $constant); if (!$this->getEnum()->isBacked()) { + /** @psalm-suppress PossiblyNullOperand */ throw new \ReflectionException( 'Enum case ' . $this->class . '::' . $this->name . ' is not a backed case' ); @@ -41,7 +56,7 @@ public function __construct($class, string $constant) /** * Gets the scalar value backing this Enum case * - * @return int|string The scalar equivalent of this enum case. + * @return mixed The scalar equivalent of this enum case. * * @link https://www.php.net/manual/en/reflectionenumbackedcase.getbackingvalue.php */ @@ -53,6 +68,10 @@ public function getBackingValue() /** * {@inheritdoc} + * + * @return SplUnitEnum The enum case object described by this reflection object. + * + * @psalm-suppress ImpureMethodCall */ public function getValue(): SplUnitEnum { diff --git a/Reflection/SplReflectionEnumHelper.php b/Reflection/SplReflectionEnumHelper.php index 0095591..b0f9b88 100644 --- a/Reflection/SplReflectionEnumHelper.php +++ b/Reflection/SplReflectionEnumHelper.php @@ -20,13 +20,105 @@ */ final class SplReflectionEnumHelper { - private static ?\ReflectionNamedType $rnts = null; - private static ?\ReflectionNamedType $rnti = null; + /** + * Array of \ReflectionNamedType for types + * + * @var \ReflectionNamedType[] + * + * @phpstan-var array + */ + private static array $rnt = []; + /** + * @psalm-suppress UnusedConstructor + */ private function __construct() { } + /** + * Get \ReflectionNamedType from internal ReflectionFunction closure. + * + * @param \ReflectionFunction $func + * + * @return \ReflectionNamedType + */ + private static function getReflectionNamedType(\ReflectionFunction $func): \ReflectionNamedType + { + /** @var \ReflectionNamedType $type */ + $type = ($func->getParameters()[0])->getType(); + + return $type; + } + + /** + * Get SplReflectionNamedType from a type. + * + * @param string $type + * @return \ReflectionNamedType + * + * @phpstan-param non-empty-string $type + * @phpstan-return \ReflectionNamedType + */ + private static function getTypedReflectionNamedType(string $type): \ReflectionNamedType + { + if (!isset(static::$rnt[$type])) { + static::$rnt[$type] = new SplReflectionNamedType($type); + } + + return static::$rnt[$type]; + } + + /** + * Return ReflectionNamedType from a $type (Internal use). + * + * @param string $type + * @return \ReflectionNamedType + */ + public static function getReflectionNamedTypeFromType(string $type): \ReflectionNamedType + { + switch ($type) { + case 'string': + $result = self::getStringReflectionNamedType(); + break; + + case 'integer': + case 'int': + $result = self::getIntReflectionNamedType(); + break; + + case 'double': + case 'float': + $result = self::getFloatReflectionNamedType(); + break; + + case 'boolean': + case 'bool': + $result = self::getBoolReflectionNamedType(); + break; + + case 'array': + $result = self::getArrayReflectionNamedType(); + break; + + case 'object': + $result = self::getObjectReflectionNamedType(); + break; + + case 'mixed': + case 'unknown type': + $result = self::getTypedReflectionNamedType('mixed'); + break; + + default: + /** @phpstan-var non-empty-string $type */ + $result = self::getTypedReflectionNamedType($type); + break; + } + + return $result; + } + /** * Only way to generate a string ReflectionNamedType (Internal use). * @@ -34,14 +126,13 @@ private function __construct() */ public static function getStringReflectionNamedType(): \ReflectionNamedType { - if (null === static::$rnts) { - $func = new \ReflectionFunction(static fn (string $param): string => $param); - // @phpstan-ignore-next-line - static::$rnts = ($func->getParameters()[0])->getType(); + if (!isset(static::$rnt['string'])) { + static::$rnt['string'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn(string $param): string => $param) + ); } - // @phpstan-ignore-next-line - return static::$rnts; + return static::$rnt['string']; } /** @@ -51,13 +142,76 @@ public static function getStringReflectionNamedType(): \ReflectionNamedType */ public static function getIntReflectionNamedType(): \ReflectionNamedType { - if (null === static::$rnti) { - $func = new \ReflectionFunction(static fn (int $param): int => $param); - // @phpstan-ignore-next-line - static::$rnti = ($func->getParameters()[0])->getType(); + if (!isset(static::$rnt['int'])) { + static::$rnt['int'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn (int $param): int => $param) + ); + } + + return static::$rnt['int']; + } + + /** + * Only way to generate a float ReflectionNamedType (Internal use). + * + * @return \ReflectionNamedType + */ + public static function getFloatReflectionNamedType(): \ReflectionNamedType + { + if (!isset(static::$rnt['float'])) { + static::$rnt['float'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn(float $param): float => $param) + ); + } + + return static::$rnt['float']; + } + + /** + * Only way to generate a bool ReflectionNamedType (Internal use). + * + * @return \ReflectionNamedType + */ + public static function getBoolReflectionNamedType(): \ReflectionNamedType + { + if (!isset(static::$rnt['bool'])) { + static::$rnt['bool'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn(bool $param): bool => $param) + ); + } + + return static::$rnt['bool']; + } + + /** + * Only way to generate an array ReflectionNamedType (Internal use). + * + * @return \ReflectionNamedType + */ + public static function getArrayReflectionNamedType(): \ReflectionNamedType + { + if (!isset(static::$rnt['array'])) { + static::$rnt['array'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn(array $param): array => $param) + ); + } + + return static::$rnt['array']; + } + + /** + * Only way to generate an object ReflectionNamedType (Internal use). + * + * @return \ReflectionNamedType + */ + public static function getObjectReflectionNamedType(): \ReflectionNamedType + { + if (!isset(static::$rnt['object'])) { + static::$rnt['object'] = static::getReflectionNamedType( + new \ReflectionFunction(static fn(object $param): object => $param) + ); } - // @phpstan-ignore-next-line - return static::$rnti; + return static::$rnt['object']; } } diff --git a/Reflection/SplReflectionEnumProxy.php b/Reflection/SplReflectionEnumProxy.php index 48845e3..17a9afb 100644 --- a/Reflection/SplReflectionEnumProxy.php +++ b/Reflection/SplReflectionEnumProxy.php @@ -14,6 +14,7 @@ namespace Ducks\Component\SplTypes\Reflection; use Ducks\Component\SplTypes\SplBackedEnum; +use Ducks\Component\SplTypes\SplEnumerable; use Ducks\Component\SplTypes\SplUnitEnum; final class SplReflectionEnumProxy @@ -44,17 +45,41 @@ final class SplReflectionEnumProxy /** * Array of constants class, indexed by name, as enum cases. * - * @var array + * @var \ReflectionClassConstant[] + * + * @phpstan-var array */ private array $constantCases = []; /** * Array of Reflection enum cases, indexed by name. * - * @var array + * @var (SplReflectionEnumUnitCase|SplReflectionEnumBackedCase)[] + * + * @phpstan-var array */ private array $cases = []; + /** + * The nameof the case beeing instanciate + * + * @var string|null + */ + private ?string $running = null; + + /** + * Name of the class constant. + * + * @var string + * + * @readonly + * + * @phpstan-var class-string + * + * @psalm-readonly + * + * @phan-read-only + */ public string $name; /** @@ -65,14 +90,16 @@ final class SplReflectionEnumProxy public function __construct(\ReflectionClass $class) { $this->class = $class; - $this->name = $class->name; + $this->name = $class->getName(); } /** * Gets constants. * - * @return array An array of constants, + * @return mixed[] An array of constants, * where the keys hold the name and the values the value of the constants. + * + * @phpstan-return array */ public function getConstants(): array { @@ -82,7 +109,7 @@ public function getConstants(): array /** * Gets a ReflectionClassConstant for a class's property * - * @param string $name ? The class constant name. + * @param string $name The class constant name. * * @return \ReflectionClassConstant|null */ @@ -115,6 +142,8 @@ private function initConstantCases(): void * @param \ReflectionClassConstant ...$constants * * @return void + * + * @no-named-arguments */ public function addConstantCase(\ReflectionClassConstant ...$constants): void { @@ -124,7 +153,7 @@ public function addConstantCase(\ReflectionClassConstant ...$constants): void !isset($this->constantCases[$name]) && $constant->isPublic() // Check consistency because of polyfilling or other bad overrides - && $constant->getDeclaringClass()->name === $this->name + && $constant->getDeclaringClass()->getName() === $this->name // Do not use isBacked method because of infinite loop possibility // Add if not BackedEnum or Backed but valid type && ( @@ -143,7 +172,9 @@ public function addConstantCase(\ReflectionClassConstant ...$constants): void /** * Return an array of class constants, indexed by name, that could be use as an enum case. * - * @return array + * @return \ReflectionClassConstant[] + * + * @phpstan-return array */ public function getConstantCases(): array { @@ -232,32 +263,30 @@ public function addCase(\ReflectionClassConstant ...$constants): void $name = $constant->getName(); if ( !isset($this->cases[$name]) + && \is_a($this->name, SplEnumerable::class, true) ) { // Check type $value = $constant->getValue(); + // Mandatory in order to prevent infinite loop + $this->running = $name; + + if (!$this->isBacked() && null === $value) { + $this->cases[$name] = new SplReflectionEnumUnitCase($this->name, $name); + unset($this->running); + continue; + } + + $backingType = $this->getBackingType(); + if ( - ( - // Accept nullable value for UnitEnum - null === $value && !($this->getBackingType() instanceof \ReflectionNamedType) - ) || ( - // Filter acceptable value for BackedEnum - $this->getBackingType() instanceof \ReflectionNamedType - && isset($value) - && ( - \is_scalar($value) && \call_user_func('is_' . $this->getBackingType(), $value) - || \is_a($value, (string) $this->getBackingType()) - ) - ) + $this->isBacked() + && $backingType instanceof \ReflectionNamedType + && $backingType->getName() === \gettype($value) ) { - // Mandatory in order to prevent infinite loop - $this->cases[$name] = true; - $case = $this->isBacked() - ? new SplReflectionEnumBackedCase($this->name, $name) - : new SplReflectionEnumUnitCase($this->name, $name); - - // Now link correct class on pointer - $this->cases[$name] = $case; + $this->cases[$name] = new SplReflectionEnumBackedCase($this->name, $name); + unset($this->running); + continue; } } } @@ -266,7 +295,9 @@ public function addCase(\ReflectionClassConstant ...$constants): void /** * Returns a list of all cases on an Enum * - * @return array + * @return (SplReflectionEnumUnitCase|SplReflectionEnumBackedCase)[] + * + * @phpstan-return array * * @link https://www.php.net/manual/en/reflectionenum.getcases.php */ @@ -313,7 +344,8 @@ public function getCase(string $name): SplReflectionEnumUnitCase */ public function hasCase(string $name): bool { - if (isset($this->cases[$name])) { + // $this->cases could be empty + if (isset($this->cases[$name]) || $this->running === $name) { return true; } @@ -339,7 +371,7 @@ public function isBacked(): bool $this->backed = false; } else { $constant = $this->getFirstCaseConstant(); - $this->backed = null !== $constant->getValue(); + $this->backed = $constant instanceof \ReflectionClassConstant && null !== $constant->getValue(); } } @@ -364,19 +396,9 @@ public function getBackingType(): ?\ReflectionNamedType } else { $constant = $this->getFirstCaseConstant(); if ($constant instanceof \ReflectionClassConstant) { - switch (\gettype($constant->getValue())) { - case 'string': - $this->backingType = SplReflectionEnumHelper::getStringReflectionNamedType(); - break; - - case 'integer': - $this->backingType = SplReflectionEnumHelper::getIntReflectionNamedType(); - break; - - default: - $this->backingType = false; - break; - } + $this->backingType = SplReflectionEnumHelper::getReflectionNamedTypeFromType( + \gettype($constant->getValue()) + ); } } } else { diff --git a/Reflection/SplReflectionEnumUnitCase.php b/Reflection/SplReflectionEnumUnitCase.php index 0f6aa4a..0e840ce 100644 --- a/Reflection/SplReflectionEnumUnitCase.php +++ b/Reflection/SplReflectionEnumUnitCase.php @@ -17,13 +17,21 @@ /** * The SplReflectionEnumUnitCase class reports information about an SplEnum unit case, which has no scalar equivalent. + * + * @property-read class-string<\Ducks\Component\SplTypes\SplEnumerable> $class + * + * @psalm-api + * @psalm-immutable + * @psalm-suppress PropertyNotSetInConstructor */ class SplReflectionEnumUnitCase extends \ReflectionClassConstant { /** * Internal instances enum * - * @var array> + * @var array[] + * + * @phpstan-var array> */ private static array $instances = []; @@ -33,7 +41,14 @@ class SplReflectionEnumUnitCase extends \ReflectionClassConstant * @param object|string $class An enum instance or a name. * @param string $constant An enum constant name. * - * @throws \ReflectionException if $class is not a SplReflectionEnumUnitCase + * @throws \ReflectionException if $class is not a \Ducks\Component\SplTypes\SplEnumerable + * + * @phpcs:ignore Generic.Files.LineLength.TooLong + * @phpstan-param \Ducks\Component\SplTypes\SplEnumerable|class-string<\Ducks\Component\SplTypes\SplEnumerable> $class An enum instance or a name. + * @phpstan-param string $constant An enum constant name. + * + * @psalm-suppress UninitializedProperty + * @psalm-suppress ImpureMethodCall * * @link https://www.php.net/manual/en/reflectionenumunitcase.construct.php */ @@ -42,6 +57,7 @@ public function __construct($class, string $constant) parent::__construct($class, $constant); if (!$this->getEnum()->hasCase($constant)) { + /** @psalm-suppress PossiblyNullOperand */ throw new \ReflectionException( 'Enum case ' . $this->class . '::' . $this->name . ' is not a case' ); @@ -53,12 +69,18 @@ public function __construct($class, string $constant) * * @return SplReflectionEnum instance describing the Enum this case belongs to. * - * @phpstan-return SplReflectionEnum + * @throws \ReflectionException if objectOrClass is not a \Ducks\Component\SplTypes\SplEnumerable * + * @phpstan-return SplReflectionEnum<\Ducks\Component\SplTypes\SplEnumerable> + * + * @psalm-suppress ArgumentTypeCoercion Needed because we need to throw Error on wrong type + * + * @see SplReflectionEnum::__construct() * @link https://www.php.net/manual/en/reflectionenumunitcase.getenum.php */ public function getEnum(): SplReflectionEnum { + /** @var SplReflectionEnum<\Ducks\Component\SplTypes\SplEnumerable> */ return new SplReflectionEnum($this->class); } @@ -66,14 +88,36 @@ public function getEnum(): SplReflectionEnum * Gets the enum case object described by this reflection object * * @return SplUnitEnum The enum case object described by this reflection object. + * + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement + * @psalm-suppress ImpureMethodCall + * @psalm-suppress ImpureStaticProperty */ public function getValue(): SplUnitEnum { if (!isset(self::$instances[$this->class][$this->name])) { - $class = $this->getDeclaringClass(); - $instance = $class->newInstanceWithoutConstructor(); + self::$instances[$this->class][$this->name] = $this->getEnumValue(); + } + + return self::$instances[$this->class][$this->name]; + } + + /** + * Gets the enum from the class and constant name. + * + * @return SplUnitEnum + * + * @psalm-suppress ImpureMethodCall + */ + private function getEnumValue(): SplUnitEnum + { + $class = $this->getDeclaringClass(); + /** @var SplUnitEnum $instance */ + $instance = $class->newInstanceWithoutConstructor(); - $object = $class->getConstructor(); + $object = $class->getConstructor(); + if ($object instanceof \ReflectionMethod) { $object->setAccessible(true); $object->invoke($instance); @@ -82,10 +126,8 @@ public function getValue(): SplUnitEnum $property->setAccessible(true); $property->setValue($instance, $this->name); } - - self::$instances[$this->class][$this->name] = $instance; } - return self::$instances[$this->class][$this->name]; + return $instance; } } diff --git a/Reflection/SplReflectionNamedType.php b/Reflection/SplReflectionNamedType.php new file mode 100644 index 0000000..4b5299d --- /dev/null +++ b/Reflection/SplReflectionNamedType.php @@ -0,0 +1,112 @@ +naming = $name; + + // Force nullable for native type name + switch (\strtolower($name)) { + case 'mixed': + case 'null': + $this->nullable = true; + break; + default: + $this->nullable = $nullable; + break; + } + } + + /** + * @inheritDoc + * + * @see https://www.php.net/manual/en/reflectionnamedtype.getname.php + */ + public function getName(): string + { + return $this->naming; + } + + /** + * @inheritDoc + * + * @see https://php.net/manual/en/reflectiontype.isbuiltin.php + */ + public function isBuiltin(): bool + { + return \in_array(\strtolower($this->getName()), self::BUILT_IN_TYPES); + } + + /** + * @inheritDoc + * + * @see https://www.php.net/manual/fr/reflectiontype.allowsnull.php + */ + public function allowsNull(): bool + { + return $this->nullable; + } + + /** + * @inheritDoc + * + * @see https://www.php.net/manual/fr/reflectiontype.tostring.php + */ + public function __toString(): string + { + return $this->getName(); + } +} diff --git a/Resources/functions.php b/Resources/functions.php index 5b9138b..4c9ddae 100644 --- a/Resources/functions.php +++ b/Resources/functions.php @@ -17,6 +17,8 @@ * @disregard P1009 Undefined type * * @phpstan-ignore-next-line + * + * @psalm-suppress UndefinedClass */ if (!\function_exists(spl_enum_exists::class)) { /** diff --git a/SplBackedEnum.php b/SplBackedEnum.php index 1d26efa..00e0c97 100644 --- a/SplBackedEnum.php +++ b/SplBackedEnum.php @@ -22,4 +22,27 @@ */ interface SplBackedEnum extends SplUnitEnum { + // public readonly string|int|mixed $value; + + /** + * Maps a scalar to an enum instance. + * + * @param int|string $value The scalar value to map to an enum case. + * + * @return static A case instance of this enumeration. + * + * @throws \ValueError if $value is not a valid backing value for enum + */ + public static function from($value): self; + + /** + * Maps a scalar to an enum instance or null. + * + * @param int|string|mixed $value e scalar value to map to an enum case. + * + * @return static|null A case instance of this enumeration, or null if not found. + * + * @psalm-suppress UnusedVariable + */ + public static function tryFrom($value): ?self; } diff --git a/SplBackedEnumTrait.php b/SplBackedEnumTrait.php index 4d0a92b..cb729ea 100644 --- a/SplBackedEnumTrait.php +++ b/SplBackedEnumTrait.php @@ -20,6 +20,8 @@ * * @template T * + * @property-read mixed $value + * * @phpstan-require-implements SplBackedEnum */ trait SplBackedEnumTrait @@ -54,12 +56,15 @@ final public static function from($value): self * @param int|string|mixed $value e scalar value to map to an enum case. * * @return static|null A case instance of this enumeration, or null if not found. + * + * @psalm-suppress UnusedVariable */ final public static function tryFrom($value): ?self { foreach (static::cases() as $case) { /** * @var SplEnumBacked $case + * * @phpstan-var SplEnumBacked $case */ if ($case->value === $value) { @@ -68,7 +73,7 @@ final public static function tryFrom($value): ?self } } - /** @var static $result */ + /** @var static|null $result */ return $result ?? null; } @@ -76,7 +81,7 @@ final public static function tryFrom($value): ?self * Return a new instance of enum. * * @param string $name - * @param array $arguments + * @param mixed[] $arguments * * @return static self keywords not an equivalent * diff --git a/SplBool.php b/SplBool.php index 32982f1..90dcbbd 100644 --- a/SplBool.php +++ b/SplBool.php @@ -19,11 +19,14 @@ * @extends SplEnum * @implements SplBackedEnum * + * @property-read mixed $value + * @property-read string $name + * * @psalm-api * @psalm-suppress MissingDependency * @psalm-suppress UndefinedClass */ -class SplBool extends SplEnum implements SplBackedEnum +class SplBool extends SplEnum implements SplBackedEnum, SplEnumSingletonable { /** @use SplBackedEnumTrait */ use SplBackedEnumTrait; diff --git a/SplEnum.php b/SplEnum.php index 6d609b9..1700c95 100644 --- a/SplEnum.php +++ b/SplEnum.php @@ -17,14 +17,14 @@ * SplEnum gives the ability to emulate and create enumeration objects natively in PHP. * * @template T - * @extends SplType + * @extends SplType * * @psalm-api * * @psalm-suppress MissingDependency * @psalm-suppress UndefinedClass */ -abstract class SplEnum extends SplType +abstract class SplEnum extends SplType implements SplEnumerable { use SplEnumTrait; /** @use SplEnumAccessorsTrait */ @@ -36,6 +36,9 @@ abstract class SplEnum extends SplType * @param mixed $initial_value * @param bool $strict * + * @phpstan-param T|null $initial_value + * @phpstan-param bool $strict + * * @throws \UnexpectedValueException if incompatible type is given. * * @SuppressWarnings(PHPMD.CamelCaseParameterName) @@ -43,6 +46,7 @@ abstract class SplEnum extends SplType */ public function __construct($initial_value = self::__default, bool $strict = true) { + /** @var T $initial_value */ $initial_value ??= static::__default; if (!\in_array($initial_value, $this->getConstList(), $strict)) { @@ -57,7 +61,7 @@ public function __construct($initial_value = self::__default, bool $strict = tru * * @param bool $include_default Whether to include __default property. * - * @return array + * @return mixed[] * * @SuppressWarnings(PHPMD.CamelCaseParameterName) * @SuppressWarnings(PHPMD.CamelCaseVariableName) diff --git a/SplEnumAccessorsTrait.php b/SplEnumAccessorsTrait.php index 0d230fc..e3b02b3 100644 --- a/SplEnumAccessorsTrait.php +++ b/SplEnumAccessorsTrait.php @@ -28,8 +28,6 @@ trait SplEnumAccessorsTrait * @param string $name * * @return mixed - * - * @phpstan-return T|null */ final public function __get(string $name) { @@ -53,11 +51,17 @@ final public function __get(string $name) /** * Writing data to inaccessible (protected or private) or non-existing properties. * - * @throws \Error + * @param string $name + * @param mixed $value * - * @psalm-suppress MissingParamType + * @return void + * + * @throws \Error * * @phpstan-ignore-next-line + * + * @psalm-suppress MissingParamType + * @psalm-suppress UnusedParam */ final public function __set(string $name, $value): void { diff --git a/SplEnumBacked.php b/SplEnumBacked.php index 2ba9142..9fba7e9 100644 --- a/SplEnumBacked.php +++ b/SplEnumBacked.php @@ -34,6 +34,8 @@ abstract class SplEnumBacked extends SplEnumUnit implements SplBackedEnum /** * {@inheritdoc} + * + * @psalm-suppress UnsupportedPropertyReferenceUsage */ protected function __construct() { diff --git a/SplEnumBackedTrait.php b/SplEnumBackedTrait.php index 109ec03..d2f40f2 100644 --- a/SplEnumBackedTrait.php +++ b/SplEnumBackedTrait.php @@ -26,15 +26,20 @@ trait SplEnumBackedTrait { use SplEnumUnitTrait { - SplEnumUnitTrait::__serialize as __unitSerialize; - SplEnumUnitTrait::__unserialize as __unitUnserialize; - SplEnumUnitTrait::__set_state as __unitSetState; + SplEnumUnitTrait::__serialize as private __unitSerialize; + SplEnumUnitTrait::__unserialize as private __unitUnserialize; + SplEnumUnitTrait::__set_state as private __unitSetState; + SplEnumUnitTrait::__debugInfo as private __unitDebugInfo; } /** * Serialize object. * - * @return array + * @return mixed[] + * + * @phpstan-return array + * + * @psalm-suppress LessSpecificImplementedReturnType */ public function __serialize(): array { @@ -46,9 +51,14 @@ public function __serialize(): array /** * Unserialize object. * - * @param array $data + * @param mixed[] $data * * @return void + * + * @phpstan-param array $data + * @phpstan-return void + * + * @psalm-suppress UnsupportedPropertyReferenceUsage */ public function __unserialize(array $data): void { @@ -60,34 +70,47 @@ public function __unserialize(array $data): void /** * Instanciate an exported object. * - * @param array $properties + * @param mixed[] $properties * * @return static * + * @phpstan-param array $properties + * @phpstan-return static + * * @psalm-suppress UnsafeInstantiation */ #[\ReturnTypeWillChange] public static function __set_state(array $properties): SplBackedEnum { - /** @var static $object */ - $object = static::__unitSetState($properties); - $object->value = $properties['value']; + /** @phpstan-var T $value */ + $value = $properties['value']; + + $object = self::__unitSetState($properties); + $object->value = $value; + /** @var static $object */ return $object; } /** * Dumping object. * - * @return array + * @return mixed[] + * + * @phpstan-return array + * + * @psalm-suppress LessSpecificImplementedReturnType * * @codeCoverageIgnore */ public function __debugInfo(): array { - return [ - 'name' => $this->name, - 'value' => $this->value, - ]; + /** @phpstan-var T $value */ + $value = $this->value; + + $result = $this->__unitDebugInfo(); + $result['value'] = $value; + + return $result; } } diff --git a/SplEnumSingletonTrait.php b/SplEnumSingletonTrait.php index 04cf06d..a504950 100644 --- a/SplEnumSingletonTrait.php +++ b/SplEnumSingletonTrait.php @@ -14,16 +14,19 @@ /** * Trait used for enum emulation * + * @phpstan-require-extends SplEnum + * @phpstan-require-implements SplEnumSingletonable + * * @psalm-api */ trait SplEnumSingletonTrait { /** - * Undocumented variable + * internal instance of enum * - * @var array + * @var SplEnumSingletonable[] * - * @phpstan-var list + * @phpstan-var array */ private static array $instances = []; @@ -33,11 +36,17 @@ trait SplEnumSingletonTrait * @param string $name * * @return SplEnumSingletonable + * + * @throws \Error if $name is not a case + * + * @see SplEnum::__callStatic() */ public static function getInstance(string $name): SplEnumSingletonable { if (!isset(self::$instances[$name])) { - self::$instances[$name] = static::$name(); + /** @var SplEnumSingletonable $instance */ + $instance = static::$name(); + self::$instances[$name] = $instance; } return self::$instances[$name]; diff --git a/SplEnumTrait.php b/SplEnumTrait.php index 1c430fe..cb36f73 100644 --- a/SplEnumTrait.php +++ b/SplEnumTrait.php @@ -24,10 +24,12 @@ trait SplEnumTrait * Return a new instance of enum * * @param string $name - * @param array $arguments + * @param mixed[] $arguments * * @return static * + * @throws \Error if $name is not a case + * * @phpstan-param string $name * @phpstan-param list $arguments * @phpstan-return static diff --git a/SplEnumUnit.php b/SplEnumUnit.php index c3190d8..7b121dc 100644 --- a/SplEnumUnit.php +++ b/SplEnumUnit.php @@ -33,5 +33,6 @@ abstract class SplEnumUnit extends SplEnum implements */ protected function __construct() { + // Cannot instantiate enum static::class } } diff --git a/SplEnumUnitTrait.php b/SplEnumUnitTrait.php index aa73bde..2c9c631 100644 --- a/SplEnumUnitTrait.php +++ b/SplEnumUnitTrait.php @@ -26,7 +26,9 @@ trait SplEnumUnitTrait /** * Serialize object. * - * @return array + * @return mixed[] + * + * @phpstan-return array */ public function __serialize(): array { @@ -39,32 +41,40 @@ public function __serialize(): array /** * Unserialize object. * - * @param array $data + * @param mixed[] $data * * @return void + * + * @phpstan-param array $data + * @phpstan-return void */ public function __unserialize(array $data): void { parent::__unserialize($data); - $this->name = $data['name']; + $this->name = (string) $data['name']; } /** * Instanciate an exported object. * - * @param array $properties + * @param mixed[] $properties * * @return static * + * @phpstan-param array $properties + * @phpstan-return static + * * @psalm-suppress UnsafeInstantiation */ public static function __set_state(array $properties): SplUnitEnum { - /** @var static $object */ + /** + * @var static $object + */ // @phpstan-ignore-next-line $object = /** @scrutinizer ignore-call */ new static(); - $object->name = $properties['name']; + $object->name = (string) $properties['name']; return $object; } @@ -72,14 +82,17 @@ public static function __set_state(array $properties): SplUnitEnum /** * Dumping object. * - * @return array + * @return string[] + * + * @phpstan-return array * * @codeCoverageIgnore */ public function __debugInfo(): array { - return [ - 'name' => $this->name, - ]; + $result = parent::__debugInfo(); + $result['name'] = $this->name; + + return $result; } } diff --git a/SplEnumerable.php b/SplEnumerable.php new file mode 100644 index 0000000..61b78e5 --- /dev/null +++ b/SplEnumerable.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Ducks\Component\SplTypes; + +/** + * Interface used in order to set a class enumerable. + * + * @psalm-api + */ +interface SplEnumerable +{ + // public readonly string $name; +} diff --git a/SplStringEnum.php b/SplStringEnum.php index 532f1b5..f818064 100644 --- a/SplStringEnum.php +++ b/SplStringEnum.php @@ -39,6 +39,13 @@ final protected function __construct() parent::__construct(); } + /** + * Method called when a script tries to call an object as a function. + * + * @return string + * + * @psalm-suppress MixedInferredReturnType + */ final public function &__invoke(): string { return $this->__default; diff --git a/SplType.php b/SplType.php index eb84aba..2450117 100644 --- a/SplType.php +++ b/SplType.php @@ -29,6 +29,8 @@ abstract class SplType implements /** * Default value. + * + * @var mixed */ // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase protected const __default = null; @@ -41,6 +43,8 @@ abstract class SplType implements * * @return void * + * @phpstan-param T|null $initial_value Type and default value depends on the extension class. + * @phpstan-param bool $strict Whether to set the object's sctrictness. * @phpstan-ignore-next-line * * @psalm-suppress PossiblyUnusedParam @@ -51,6 +55,7 @@ abstract class SplType implements */ public function __construct($initial_value = self::__default, /** @scrutinizer ignore-unused */ bool $strict = true) { + /** @var T $this->__default */ $this->__default = $initial_value ?? static::__default; } } diff --git a/SplTypeTrait.php b/SplTypeTrait.php index 0761a36..ce7a923 100644 --- a/SplTypeTrait.php +++ b/SplTypeTrait.php @@ -48,7 +48,7 @@ public function jsonSerialize() /** * Serialize object. * - * @return array + * @return mixed[] * * @phpstan-return array */ @@ -62,12 +62,16 @@ public function __serialize(): array /** * Unserialize object. * - * @param array $data + * @param mixed[] $data * * @return void + * + * @phpstan-param array $data + * @phpstan-return void */ public function __unserialize(array $data): void { + /** @phpstan-var T $data['__default'] */ $this->__default = $data['__default']; } @@ -75,6 +79,8 @@ public function __unserialize(array $data): void * Method called when a script tries to call an object as a function. * * @return mixed + * + * @phpstan-return T */ #[\ReturnTypeWillChange] public function &__invoke() @@ -95,7 +101,7 @@ final public function __toString(): string /** * Instanciate an exported object. * - * @param array $properties + * @param mixed[] $properties * * @return static * @@ -103,6 +109,7 @@ final public function __toString(): string * @phpstan-return static * * @psalm-suppress UnsafeInstantiation + * @psalm-suppress UndefinedDocblockClass */ public static function __set_state(array $properties): object { @@ -113,7 +120,7 @@ public static function __set_state(array $properties): object /** * Dumping object. * - * @return array + * @return mixed[] * * @phpstan-return array * diff --git a/SplUnitEnum.php b/SplUnitEnum.php index f922fa2..fa02074 100644 --- a/SplUnitEnum.php +++ b/SplUnitEnum.php @@ -18,7 +18,7 @@ * * @psalm-api */ -interface SplUnitEnum +interface SplUnitEnum extends SplEnumerable { // public readonly string $name; diff --git a/SplUnitEnumTrait.php b/SplUnitEnumTrait.php index 819a968..b1fb669 100644 --- a/SplUnitEnumTrait.php +++ b/SplUnitEnumTrait.php @@ -17,6 +17,10 @@ use Ducks\Component\SplTypes\Reflection\SplReflectionEnumUnitCase; /** + * Simplify SplUnitEnum integration + * + * @property-read string $name + * * @phpstan-require-implements SplUnitEnum */ trait SplUnitEnumTrait @@ -31,7 +35,13 @@ trait SplUnitEnumTrait protected string $name; /** - * @inheritDoc + * Generates a list of cases on an enum + * + * @return static[] An array of all defined cases of this enumeration, in order of declaration. + * + * @psalm-suppress MixedInferredReturnType + * + * @see SplUnitEnum::cases() */ public static function cases(): array { @@ -40,7 +50,6 @@ public static function cases(): array if (null === $cases) { $enum = new SplReflectionEnum(static::class); foreach ($enum->getCases() as $case) { - /** @var Reflection\SplReflectionEnumUnitCase $case */ $cases[] = $case->getValue(); } } @@ -52,7 +61,7 @@ public static function cases(): array * Return a new instance of enum * * @param string $name - * @param array $arguments + * @param mixed[] $arguments * * @return static self keywords not an equivalent * diff --git a/Tests/benchmark/SplEnumBench.php b/Tests/benchmark/SplEnumBench.php index 66d4176..20358e5 100644 --- a/Tests/benchmark/SplEnumBench.php +++ b/Tests/benchmark/SplEnumBench.php @@ -50,6 +50,7 @@ public function benchCreateSeptemberMonth(): void */ public function benchCreateUnstrictMonth(): void { + // @phpstan-ignore argument.type new Month('1', false); } } diff --git a/Tests/phpunit/SplEnumTest.php b/Tests/phpunit/SplEnumTest.php index 12139a6..d40a563 100644 --- a/Tests/phpunit/SplEnumTest.php +++ b/Tests/phpunit/SplEnumTest.php @@ -31,6 +31,7 @@ public function test(): void $instance = new Month(Month::SEPTEMBER); $this->assertEquals(Month::SEPTEMBER, $instance()); + // @phpstan-ignore argument.type $instance = new Month('1', false); $this->assertEquals(Month::JANUARY, $instance()); } @@ -45,6 +46,7 @@ public function test(): void public function testUnexpectedValueRxception(): void { $this->expectException(\UnexpectedValueException::class); + // @phpstan-ignore argument.type new Month('1'); } diff --git a/Util/Tools.php b/Util/Tools.php index e3d3eea..4d5beb1 100644 --- a/Util/Tools.php +++ b/Util/Tools.php @@ -17,6 +17,9 @@ final class Tools { + /** + * @psalm-suppress UnusedConstructor + */ private function __construct() { } diff --git a/assets/documentation/Lexique.md b/assets/documentation/Lexique.md index 1168693..594c992 100644 --- a/assets/documentation/Lexique.md +++ b/assets/documentation/Lexique.md @@ -11,6 +11,7 @@ ## Interfaces +[`SplEnumerable`] [`SplUnitEnum`] [`SplBackedEnum`] @@ -28,6 +29,7 @@ [`SplEnum`]: ./classes/SplEnum.md [`SplBool`]: ./classes/SplBool.md [`SplString`]: ./classes/SplString.md +[`SplEnumerable`]: ./classes/SplEnumerable.md [`SplUnitEnum`]: ./classes/SplUnitEnum.md [`SplBackedEnum`]: ./classes/SplBackedEnum.md [`SplEnumUnit`]: ./classes/SplEnumUnit.md diff --git a/assets/documentation/classes/Reflection/SplReflectionNamedType.md b/assets/documentation/classes/Reflection/SplReflectionNamedType.md new file mode 100644 index 0000000..e69de29 diff --git a/assets/documentation/classes/SplEnumerable.md b/assets/documentation/classes/SplEnumerable.md new file mode 100644 index 0000000..febf191 --- /dev/null +++ b/assets/documentation/classes/SplEnumerable.md @@ -0,0 +1,16 @@ +# [The SplEnumerable interface](#The-SplEnumerable-interface) + +(PHP 7, PHP 8) + +## [Introduction](#Introduction) + +Interface used in order to set a class enumerable. + +## [Interface synopsis](#Interface-synopsis) + +```php +interface SplEnumerable { +} +``` + +## [Table of Contents](#Table-of-Contents) diff --git a/assets/documentation/classes/SplUnitEnum.md b/assets/documentation/classes/SplUnitEnum.md index 49cf4b5..7cd777f 100644 --- a/assets/documentation/classes/SplUnitEnum.md +++ b/assets/documentation/classes/SplUnitEnum.md @@ -4,14 +4,14 @@ ## [Introduction](#Introduction) -Interface used in order to implements \UnitEnum one. +Interface used in order to act like \UnitEnum one. ## [Interface synopsis](#Interface-synopsis) ```php -interface SplUnitEnum extends UnitEnum { - /* Inherited methods */ - public static UnitEnum::cases(): array +interface SplUnitEnum extends SplEnumerable { + /* Methods */ + public static SplUnitEnum::cases(): array } ``` diff --git a/composer.json b/composer.json index ae993de..ecc96be 100755 --- a/composer.json +++ b/composer.json @@ -89,7 +89,7 @@ "phpcsfixer-check": "./vendor/bin/php-cs-fixer fix --dry-run --diff", "phpmd": "./vendor/bin/phpmd . github phpmd.xml.dist --exclude 'Tests/*,vendor/*,.history/*'", "phpstan": "./vendor/bin/phpstan analyse --configuration phpstan.neon.dist", - "psalm": "./vendor/bin/psalm --config=psalm.xml.dist --no-cache --no-file-cache", + "psalm": "./vendor/bin/psalm --config=psalm.xml.dist --no-cache --no-file-cache --shepherd", "rector": "./vendor/bin/rector process . --dry-run", "test": [ "@unittest", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5fb5caf..7237a97 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,7 +1,9 @@ parameters: - level: 6 + level: 8 paths: - . excludePaths: - vendor/* - .history/* + tips: + treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 852eb78..239a9c1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,6 +8,7 @@ ./Resources ./Tests ./vendor + ./.history ./.php-cs-fixer.dist.php ./bootstrap.php ./rector.php diff --git a/psalm.xml.dist b/psalm.xml.dist index 8cd34b3..290cc58 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -1,6 +1,6 @@ - - - - - - - + + + + +