diff --git a/docs/pages/mapping/handled-types.md b/docs/pages/mapping/handled-types.md index 49232c2..437ba7e 100644 --- a/docs/pages/mapping/handled-types.md +++ b/docs/pages/mapping/handled-types.md @@ -152,6 +152,30 @@ final class SomeClass } ``` +### Class constants + +```php +final class SomeClassWithConstants +{ + public const FOO = 1337; + + public const BAR = 'bar'; + + public const BAZ = 'baz'; +} + +final class SomeClass +{ + public function __construct( + /** @var SomeClassWithConstants::FOO|SomeClassWithConstants::BAR */ + private int|string $oneOfTwoCasesOfConstants, + + /** @param SomeClassWithConstants::BA* (matches `bar` or `baz`) */ + private string $casesOfConstantsMatchingPattern, + ) {} +} +``` + ### Enums ```php diff --git a/src/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php b/src/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php new file mode 100644 index 0000000..4e436b6 --- /dev/null +++ b/src/Type/Parser/Exception/Constant/ClassConstantCaseNotFound.php @@ -0,0 +1,24 @@ +delegate->tokenize($symbol); if ($token instanceof ClassNameToken) { - $className = $token->className(); - - return new GenericClassNameToken($className, $this->typeParserFactory, $this->templateParser); + return new GenericClassNameToken($token, $this->typeParserFactory, $this->templateParser); } return $token; diff --git a/src/Type/Parser/Lexer/Token/ClassNameToken.php b/src/Type/Parser/Lexer/Token/ClassNameToken.php index 5806e5b..050a72c 100644 --- a/src/Type/Parser/Lexer/Token/ClassNameToken.php +++ b/src/Type/Parser/Lexer/Token/ClassNameToken.php @@ -4,45 +4,128 @@ declare(strict_types=1); namespace CuyZ\Valinor\Type\Parser\Lexer\Token; +use CuyZ\Valinor\Type\Parser\Exception\Constant\ClassConstantCaseNotFound; +use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingClassConstantCase; +use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingClassConstantColon; +use CuyZ\Valinor\Type\Parser\Exception\Constant\MissingSpecificClassConstantCase; use CuyZ\Valinor\Type\Parser\Lexer\TokenStream; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\ClassType; +use CuyZ\Valinor\Type\Types\Factory\ValueTypeFactory; use CuyZ\Valinor\Type\Types\InterfaceType; +use CuyZ\Valinor\Type\Types\UnionType; use CuyZ\Valinor\Utility\Reflection\Reflection; +use ReflectionClass; +use ReflectionClassConstant; + +use function array_map; +use function array_values; +use function count; +use function explode; /** @internal */ final class ClassNameToken implements TraversingToken { - /** @var class-string */ - private string $className; + /** @var ReflectionClass */ + private ReflectionClass $reflection; /** * @param class-string $className */ public function __construct(string $className) { - $this->className = $className; + $this->reflection = Reflection::class($className); } public function traverse(TokenStream $stream): Type { - $reflection = Reflection::class($this->className); + $constant = $this->classConstant($stream); - return $reflection->isInterface() || $reflection->isAbstract() - ? new InterfaceType($this->className) - : new ClassType($this->className); - } + if ($constant) { + return $constant; + } - /** - * @return class-string - */ - public function className(): string - { - return $this->className; + return $this->reflection->isInterface() || $this->reflection->isAbstract() + ? new InterfaceType($this->reflection->name) + : new ClassType($this->reflection->name); } public function symbol(): string { - return $this->className; + return $this->reflection->name; + } + + private function classConstant(TokenStream $stream): ?Type + { + if ($stream->done() || ! $stream->next() instanceof ColonToken) { + return null; + } + + $case = $stream->forward(); + $missingColon = true; + + if (! $stream->done()) { + $case = $stream->forward(); + + $missingColon = ! $case instanceof ColonToken; + } + + if (! $missingColon) { + if ($stream->done()) { + throw new MissingClassConstantCase($this->reflection->name); + } + + $case = $stream->forward(); + } + + $symbol = $case->symbol(); + + if ($symbol === '*') { + throw new MissingSpecificClassConstantCase($this->reflection->name); + } + + if ($missingColon) { + throw new MissingClassConstantColon($this->reflection->name, $symbol); + } + + $cases = []; + + if (! preg_match('/\*\s*\*/', $symbol)) { + $finder = new CaseFinder($this->cases()); + $cases = $finder->matching(explode('*', $symbol)); + } + + if (empty($cases)) { + throw new ClassConstantCaseNotFound($this->reflection->name, $symbol); + } + + $cases = array_map(static fn ($value) => ValueTypeFactory::from($value), $cases); + + if (count($cases) > 1) { + // @PHP8.0 remove `array_values` + // @infection-ignore-all + return new UnionType(...array_values($cases)); + } + + return reset($cases); + } + + /** + * @return array + */ + private function cases(): array + { + // @PHP8.0 use `getConstants(ReflectionClassConstant::IS_PUBLIC)` + $cases = []; + + foreach ($this->reflection->getReflectionConstants() as $constant) { + if (! $constant->isPublic()) { + continue; + } + + $cases[$constant->name] = $constant->getValue(); + } + + return $cases; } } diff --git a/src/Type/Parser/Lexer/Token/GenericClassNameToken.php b/src/Type/Parser/Lexer/Token/GenericClassNameToken.php index 56a8c97..d61985b 100644 --- a/src/Type/Parser/Lexer/Token/GenericClassNameToken.php +++ b/src/Type/Parser/Lexer/Token/GenericClassNameToken.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace CuyZ\Valinor\Type\Parser\Lexer\Token; use CuyZ\Valinor\Type\IntegerType; -use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Parser\Exception\Generic\AssignedGenericNotFound; use CuyZ\Valinor\Type\Parser\Exception\Generic\CannotAssignGeneric; use CuyZ\Valinor\Type\Parser\Exception\Generic\GenericClosingBracketMissing; @@ -24,70 +23,74 @@ use CuyZ\Valinor\Type\StringType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\ArrayKeyType; use CuyZ\Valinor\Type\Types\ClassType; -use CuyZ\Valinor\Type\Types\InterfaceType; use CuyZ\Valinor\Utility\Reflection\Reflection; use function array_keys; use function array_shift; use function array_slice; use function count; +use function get_class; /** @internal */ final class GenericClassNameToken implements TraversingToken { - /** @var class-string */ - private string $className; - private TypeParserFactory $typeParserFactory; private TemplateParser $templateParser; - /** - * @param class-string $className - */ - public function __construct(string $className, TypeParserFactory $typeParserFactory, TemplateParser $templateParser) + private ClassNameToken $delegate; + + public function __construct(ClassNameToken $delegate, TypeParserFactory $typeParserFactory, TemplateParser $templateParser) { - $this->className = $className; + $this->delegate = $delegate; $this->typeParserFactory = $typeParserFactory; $this->templateParser = $templateParser; } - public function traverse(TokenStream $stream): ObjectType + public function traverse(TokenStream $stream): Type { - $reflection = Reflection::class($this->className); + $type = $this->delegate->traverse($stream); + + if (! $type instanceof ClassType) { + return $type; + } + + $className = $type->className(); + $reflection = Reflection::class($className); try { $docComment = $reflection->getDocComment() ?: ''; $parser = new LazyParser( fn () => $this->typeParserFactory->get( - new ClassContextSpecification($this->className), + new ClassContextSpecification($className), new AliasSpecification($reflection) ) ); $templates = $this->templateParser->templates($docComment, $parser); } catch (InvalidTemplate $exception) { - throw new InvalidClassTemplate($this->className, $exception); + throw new InvalidClassTemplate($className, $exception); } - $generics = $this->generics($stream, $templates); - $generics = $this->assignGenerics($templates, $generics); + $generics = $this->generics($stream, $className, $templates); + $generics = $this->assignGenerics($className, $templates, $generics); - return $reflection->isInterface() || $reflection->isAbstract() - ? new InterfaceType($this->className, $generics) - : new ClassType($this->className, $generics); + $typeClass = get_class($type); + + return new $typeClass($className, $generics); } public function symbol(): string { - return $this->className; + return $this->delegate->symbol(); } /** * @param array $templates + * @param class-string $className * @return Type[] */ - private function generics(TokenStream $stream, array $templates): array + private function generics(TokenStream $stream, string $className, array $templates): array { if ($stream->done() || ! $stream->next() instanceof OpeningBracketToken) { return []; @@ -99,13 +102,13 @@ final class GenericClassNameToken implements TraversingToken while (true) { if ($stream->done()) { - throw new MissingGenerics($this->className, $generics, $templates); + throw new MissingGenerics($className, $generics, $templates); } $generics[] = $stream->read(); if ($stream->done()) { - throw new GenericClosingBracketMissing($this->className, $generics); + throw new GenericClosingBracketMissing($className, $generics); } $next = $stream->forward(); @@ -115,7 +118,7 @@ final class GenericClassNameToken implements TraversingToken } if (! $next instanceof CommaToken) { - throw new GenericCommaMissing($this->className, $generics); + throw new GenericCommaMissing($className, $generics); } } @@ -123,11 +126,12 @@ final class GenericClassNameToken implements TraversingToken } /** + * @param class-string $className * @param array $templates * @param Type[] $generics * @return array */ - private function assignGenerics(array $templates, array $generics): array + private function assignGenerics(string $className, array $templates, array $generics): array { $assignedGenerics = []; @@ -137,7 +141,7 @@ final class GenericClassNameToken implements TraversingToken if ($generic === null) { $remainingTemplates = array_keys(array_slice($templates, count($assignedGenerics))); - throw new AssignedGenericNotFound($this->className, ...$remainingTemplates); + throw new AssignedGenericNotFound($className, ...$remainingTemplates); } if ($template instanceof ArrayKeyType && $generic instanceof StringType) { @@ -149,14 +153,14 @@ final class GenericClassNameToken implements TraversingToken } if (! $generic->matches($template)) { - throw new InvalidAssignedGeneric($generic, $template, $name, $this->className); + throw new InvalidAssignedGeneric($generic, $template, $name, $className); } $assignedGenerics[$name] = $generic; } if (! empty($generics)) { - throw new CannotAssignGeneric($this->className, ...$generics); + throw new CannotAssignGeneric($className, ...$generics); } return $assignedGenerics; diff --git a/src/Utility/TypeHelper.php b/src/Utility/TypeHelper.php index ebb293a..6800286 100644 --- a/src/Utility/TypeHelper.php +++ b/src/Utility/TypeHelper.php @@ -8,6 +8,7 @@ use CuyZ\Valinor\Mapper\Object\Argument; use CuyZ\Valinor\Mapper\Object\Arguments; use CuyZ\Valinor\Type\CompositeType; use CuyZ\Valinor\Type\EnumType; +use CuyZ\Valinor\Type\FixedType; use CuyZ\Valinor\Type\ObjectType; use CuyZ\Valinor\Type\Type; use CuyZ\Valinor\Type\Types\MixedType; @@ -20,6 +21,8 @@ final class TypeHelper { if ($type instanceof EnumType) { $text = $type->readableSignature(); + } elseif ($type instanceof FixedType) { + return $type->toString(); } elseif (self::containsObject($type)) { $text = '?'; } else { diff --git a/tests/Fixture/Object/ObjectWithConstants.php b/tests/Fixture/Object/ObjectWithConstants.php new file mode 100644 index 0000000..2a5d59e --- /dev/null +++ b/tests/Fixture/Object/ObjectWithConstants.php @@ -0,0 +1,63 @@ + 'some string value', + 'integer' => 1653398288, + 'float' => 1337.42, + ]; + + public const CONST_WITH_ARRAY_VALUE_B = [ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ]; + + public const CONST_WITH_NESTED_ARRAY_VALUE_A = [ + 'nested_array' => [ + 'string' => 'some string value', + 'integer' => 1653398288, + 'float' => 1337.42, + ], + ]; + + public const CONST_WITH_NESTED_ARRAY_VALUE_B = [ + 'another_nested_array' => [ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ], + ]; + + /** + * @PHP8.1 replace all calls with `ObjectWithConstants::class` + * @return class-string + */ + public static function className(): string + { + return PHP_VERSION_ID >= 8_01_00 + ? ObjectWithConstantsIncludingEnums::class + : ObjectWithConstants::class; + } +} diff --git a/tests/Fixture/Object/ObjectWithConstantsIncludingEnums.php b/tests/Fixture/Object/ObjectWithConstantsIncludingEnums.php new file mode 100644 index 0000000..3ba298c --- /dev/null +++ b/tests/Fixture/Object/ObjectWithConstantsIncludingEnums.php @@ -0,0 +1,14 @@ + IntersectionType::class, ]; + yield 'Class constant with string value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_STRING_VALUE_A', + 'transformed' => "'some string value'", + 'type' => StringValueType::class, + ]; + + yield 'Class constant with integer value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_INTEGER_VALUE_A', + 'transformed' => '1653398288', + 'type' => IntegerValueType::class, + ]; + + yield 'Class constant with float value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_FLOAT_VALUE_A', + 'transformed' => '1337.42', + 'type' => FloatValueType::class, + ]; + + if (PHP_VERSION_ID >= 8_01_00) { + yield 'Class constant with enum value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_ENUM_VALUE_A', + 'transformed' => BackedIntegerEnum::class . '::FOO', + 'type' => EnumValueType::class, + ]; + } + + yield 'Class constant with array value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_ARRAY_VALUE_A', + 'transformed' => "array{string: 'some string value', integer: 1653398288, float: 1337.42}", + 'type' => ShapedArrayType::class, + ]; + + yield 'Class constant with nested array value' => [ + 'raw' => ObjectWithConstants::className() . '::CONST_WITH_NESTED_ARRAY_VALUE_A', + 'transformed' => "array{nested_array: array{string: 'some string value', integer: 1653398288, float: 1337.42}}", + 'type' => ShapedArrayType::class, + ]; + if (PHP_VERSION_ID >= 8_01_00) { yield 'Pure enum' => [ 'raw' => PureEnum::class, @@ -1346,4 +1389,67 @@ final class NativeLexerTest extends TestCase $this->parser->parse(PureEnum::class . ':FOO'); } + + public function test_missing_class_constant_case_throws_exception(): void + { + $this->expectException(MissingClassConstantCase::class); + $this->expectExceptionCode(1664905018); + $this->expectExceptionMessage('Missing case name for class constant `' . ObjectWithConstants::className() . '::?`.'); + + $this->parser->parse(ObjectWithConstants::className() . '::'); + } + + public function test_no_class_constant_case_found_throws_exception(): void + { + $this->expectException(ClassConstantCaseNotFound::class); + $this->expectExceptionCode(1652189140); + $this->expectExceptionMessage('Unknown class constant case `' . ObjectWithConstants::className() . '::ABC`.'); + + $this->parser->parse(ObjectWithConstants::className() . '::ABC'); + } + + public function test_no_class_constant_case_found_with_wildcard_throws_exception(): void + { + $this->expectException(ClassConstantCaseNotFound::class); + $this->expectExceptionCode(1652189140); + $this->expectExceptionMessage('Cannot find class constant case with pattern `' . ObjectWithConstants::className() . '::ABC*`.'); + + $this->parser->parse(ObjectWithConstants::className() . '::ABC*'); + } + + public function test_no_class_constant_case_found_with_several_wildcards_in_a_row_throws_exception(): void + { + $this->expectException(ClassConstantCaseNotFound::class); + $this->expectExceptionCode(1652189140); + $this->expectExceptionMessage('Cannot find class constant case with pattern `' . ObjectWithConstants::className() . '::F**O`.'); + + $this->parser->parse(ObjectWithConstants::className() . '::F**O'); + } + + public function test_missing_specific_class_constant_case_throws_exception(): void + { + $this->expectException(MissingSpecificClassConstantCase::class); + $this->expectExceptionCode(1664904636); + $this->expectExceptionMessage('Missing specific case for class constant `' . ObjectWithConstants::className() . '::?` (cannot be `*`).'); + + $this->parser->parse(ObjectWithConstants::className() . '::*'); + } + + public function test_missing_class_constant_colon_and_case_throws_exception(): void + { + $this->expectException(MissingClassConstantColon::class); + $this->expectExceptionCode(1652189143); + $this->expectExceptionMessage('Missing second colon symbol for class constant `' . ObjectWithConstants::className() . '::?`.'); + + $this->parser->parse(ObjectWithConstants::className() . ':'); + } + + public function test_missing_class_constant_colon_throws_exception(): void + { + $this->expectException(MissingClassConstantColon::class); + $this->expectExceptionCode(1652189143); + $this->expectExceptionMessage('Missing second colon symbol for class constant `' . ObjectWithConstants::className() . '::FOO`.'); + + $this->parser->parse(ObjectWithConstants::className() . ':FOO'); + } } diff --git a/tests/Integration/Mapping/Fixture/NativeUnionValues.php b/tests/Integration/Mapping/Fixture/NativeUnionValues.php index 868d5e2..3ca9c3b 100644 --- a/tests/Integration/Mapping/Fixture/NativeUnionValues.php +++ b/tests/Integration/Mapping/Fixture/NativeUnionValues.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace CuyZ\Valinor\Tests\Integration\Mapping\Fixture; // @PHP8.0 move inside \CuyZ\Valinor\Tests\Integration\Mapping\UnionValuesMappingTest +use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants; + class NativeUnionValues { public bool|float|int|string $scalarWithBoolean = 'Schwifty!'; @@ -24,6 +26,12 @@ class NativeUnionValues /** @var int|false */ public int|bool $intOrLiteralFalse = 42; + + /** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */ + public string|int $constantWithStringValue = 1653398288; + + /** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */ + public string|int $constantWithIntegerValue = 'some string value'; } class NativeUnionValuesWithConstructor extends NativeUnionValues @@ -31,6 +39,8 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues /** * @param int|true $intOrLiteralTrue * @param int|false $intOrLiteralFalse + * @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithStringValue + * @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithIntegerValue */ public function __construct( bool|float|int|string $scalarWithBoolean = 'Schwifty!', @@ -40,7 +50,9 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues string|null $nullableWithString = 'Schwifty!', string|null $nullableWithNull = 'Schwifty!', int|bool $intOrLiteralTrue = 42, - int|bool $intOrLiteralFalse = 42 + int|bool $intOrLiteralFalse = 42, + string|int $constantWithStringValue = 1653398288, + string|int $constantWithIntegerValue = 'some string value' ) { $this->scalarWithBoolean = $scalarWithBoolean; $this->scalarWithFloat = $scalarWithFloat; @@ -50,5 +62,7 @@ class NativeUnionValuesWithConstructor extends NativeUnionValues $this->nullableWithNull = $nullableWithNull; $this->intOrLiteralTrue = $intOrLiteralTrue; $this->intOrLiteralFalse = $intOrLiteralFalse; + $this->constantWithStringValue = $constantWithStringValue; + $this->constantWithIntegerValue = $constantWithIntegerValue; } } diff --git a/tests/Integration/Mapping/Object/ConstantValuesMappingTest.php b/tests/Integration/Mapping/Object/ConstantValuesMappingTest.php new file mode 100644 index 0000000..4166b63 --- /dev/null +++ b/tests/Integration/Mapping/Object/ConstantValuesMappingTest.php @@ -0,0 +1,180 @@ + 'another string value', + 'constantIntegerValue' => 1653398289, + 'constantFloatValue' => 404.512, + 'constantArrayValue' => [ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ], + 'constantNestedArrayValue' => [ + 'another_nested_array' => [ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ], + ], + ]; + + // @PHP8.1 remove condition + if (PHP_VERSION_ID >= 8_01_00) { + $source['constantEnumValue'] = BackedIntegerEnum::FOO; + } + + // @PHP8.1 merge classes + $classes = PHP_VERSION_ID >= 8_01_00 + ? [ClassWithConstantValuesIncludingEnum::class, ClassWithConstantValuesIncludingEnumWithConstructor::class] + : [ClassWithConstantValues::class, ClassWithConstantValuesWithConstructor::class]; + + foreach ($classes as $class) { + try { + $result = (new MapperBuilder())->mapper()->map($class, $source); + } catch (MappingError $error) { + $this->mappingFail($error); + } + + self::assertSame('another string value', $result->constantStringValue); + self::assertSame(1653398289, $result->constantIntegerValue); + self::assertSame(404.512, $result->constantFloatValue); + + // @PHP8.1 remove condition + if (PHP_VERSION_ID >= 8_01_00) { + /** @var ClassWithConstantValuesIncludingEnum $result */ + self::assertSame(BackedIntegerEnum::FOO, $result->constantEnumValue); + } + + self::assertSame([ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ], $result->constantArrayValue); + self::assertSame([ + 'another_nested_array' => [ + 'string' => 'another string value', + 'integer' => 1653398289, + 'float' => 404.512, + ], + ], $result->constantNestedArrayValue); + } + } + + public function test_private_constant_cannot_be_mapped(): void + { + try { + (new MapperBuilder()) + ->mapper() + ->map(ObjectWithConstants::className() . '::CONST_WITH_STRING_*', 'some private string value'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1607027306', $error->code()); + self::assertSame("Value 'some private string value' does not match any of 'some string value', 'another string value'.", (string)$error); + } + } + + public function test_constant_not_matching_pattern_cannot_be_mapped(): void + { + try { + (new MapperBuilder()) + ->mapper() + ->map(ObjectWithConstants::className() . '::CONST_WITH_STRING_*', 'some prefixed string value'); + } catch (MappingError $exception) { + $error = $exception->node()->messages()[0]; + + self::assertSame('1607027306', $error->code()); + self::assertSame("Value 'some prefixed string value' does not match any of 'some string value', 'another string value'.", (string)$error); + } + } +} + +class ClassWithConstantValues +{ + /** @var ObjectWithConstants::CONST_WITH_STRING_* */ + public string $constantStringValue; + + /** @var ObjectWithConstants::CONST_WITH_INTEGER_* */ + public int $constantIntegerValue; + + /** @var ObjectWithConstants::CONST_WITH_FLOAT_* */ + public float $constantFloatValue; + + /** @var ObjectWithConstants::CONST_WITH_ARRAY_VALUE_* */ + public array $constantArrayValue; + + /** @var ObjectWithConstants::CONST_WITH_NESTED_ARRAY_VALUE_* */ + public array $constantNestedArrayValue; +} + +class ClassWithConstantValuesIncludingEnum extends ClassWithConstantValues +{ + /** @var ObjectWithConstantsIncludingEnums::CONST_WITH_ENUM_VALUE_* */ + public BackedIntegerEnum $constantEnumValue; +} + +final class ClassWithConstantValuesWithConstructor extends ClassWithConstantValues +{ + /** + * @param ObjectWithConstants::CONST_WITH_STRING_* $constantStringValue + * @param ObjectWithConstants::CONST_WITH_INTEGER_* $constantIntegerValue + * @param ObjectWithConstants::CONST_WITH_FLOAT_* $constantFloatValue + * @param ObjectWithConstants::CONST_WITH_ARRAY_VALUE_* $constantArrayValue + * @param ObjectWithConstants::CONST_WITH_NESTED_ARRAY_VALUE_* $constantNestedArrayValue + */ + public function __construct( + string $constantStringValue, + int $constantIntegerValue, + float $constantFloatValue, + array $constantArrayValue, + array $constantNestedArrayValue + ) { + $this->constantStringValue = $constantStringValue; + $this->constantIntegerValue = $constantIntegerValue; + $this->constantFloatValue = $constantFloatValue; + $this->constantArrayValue = $constantArrayValue; + $this->constantNestedArrayValue = $constantNestedArrayValue; + } +} + +final class ClassWithConstantValuesIncludingEnumWithConstructor extends ClassWithConstantValuesIncludingEnum +{ + /** + * @param ObjectWithConstants::CONST_WITH_STRING_* $constantStringValue + * @param ObjectWithConstants::CONST_WITH_INTEGER_* $constantIntegerValue + * @param ObjectWithConstants::CONST_WITH_FLOAT_* $constantFloatValue + * @param ObjectWithConstantsIncludingEnums::CONST_WITH_ENUM_VALUE_* $constantEnumValue + * @param ObjectWithConstants::CONST_WITH_ARRAY_VALUE_* $constantArrayValue + * @param ObjectWithConstants::CONST_WITH_NESTED_ARRAY_VALUE_* $constantNestedArrayValue + */ + public function __construct( + string $constantStringValue, + int $constantIntegerValue, + float $constantFloatValue, + BackedIntegerEnum $constantEnumValue, + array $constantArrayValue, + array $constantNestedArrayValue + ) { + $this->constantStringValue = $constantStringValue; + $this->constantIntegerValue = $constantIntegerValue; + $this->constantFloatValue = $constantFloatValue; + $this->constantEnumValue = $constantEnumValue; + $this->constantArrayValue = $constantArrayValue; + $this->constantNestedArrayValue = $constantNestedArrayValue; + } +} diff --git a/tests/Integration/Mapping/Object/UnionValuesMappingTest.php b/tests/Integration/Mapping/Object/UnionValuesMappingTest.php index ace6e59..2c7fa4f 100644 --- a/tests/Integration/Mapping/Object/UnionValuesMappingTest.php +++ b/tests/Integration/Mapping/Object/UnionValuesMappingTest.php @@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Tests\Integration\Mapping\Object; use CuyZ\Valinor\Mapper\MappingError; use CuyZ\Valinor\MapperBuilder; +use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants; use CuyZ\Valinor\Tests\Integration\IntegrationTest; use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValues; use CuyZ\Valinor\Tests\Integration\Mapping\Fixture\NativeUnionValuesWithConstructor; @@ -23,6 +24,8 @@ final class UnionValuesMappingTest extends IntegrationTest 'nullableWithNull' => null, 'intOrLiteralTrue' => true, 'intOrLiteralFalse' => false, + 'constantWithStringValue' => 'some string value', + 'constantWithIntegerValue' => 1653398288, ]; $classes = [UnionValues::class, UnionValuesWithConstructor::class]; @@ -47,6 +50,8 @@ final class UnionValuesMappingTest extends IntegrationTest self::assertSame(null, $result->nullableWithNull); self::assertSame(true, $result->intOrLiteralTrue); self::assertSame(false, $result->intOrLiteralFalse); + self::assertSame('some string value', $result->constantWithStringValue); + self::assertSame(1653398288, $result->constantWithIntegerValue); } $source = [ @@ -100,6 +105,12 @@ class UnionValues /** @var int|false */ public $intOrLiteralFalse = 42; + + /** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */ + public $constantWithStringValue = 1653398288; + + /** @var ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A */ + public $constantWithIntegerValue = 'some string value'; } class UnionOfFixedValues @@ -134,6 +145,8 @@ class UnionValuesWithConstructor extends UnionValues * @param string|null|float $nullableWithNull * @param int|true $intOrLiteralTrue * @param int|false $intOrLiteralFalse + * @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithStringValue + * @param ObjectWithConstants::CONST_WITH_STRING_VALUE_A|ObjectWithConstants::CONST_WITH_INTEGER_VALUE_A $constantWithIntegerValue */ public function __construct( $scalarWithBoolean = 'Schwifty!', @@ -143,7 +156,9 @@ class UnionValuesWithConstructor extends UnionValues $nullableWithString = 'Schwifty!', $nullableWithNull = 'Schwifty!', $intOrLiteralTrue = 42, - $intOrLiteralFalse = 42 + $intOrLiteralFalse = 42, + $constantWithStringValue = 1653398288, + $constantWithIntegerValue = 'some string value' ) { $this->scalarWithBoolean = $scalarWithBoolean; $this->scalarWithFloat = $scalarWithFloat; @@ -153,6 +168,8 @@ class UnionValuesWithConstructor extends UnionValues $this->nullableWithNull = $nullableWithNull; $this->intOrLiteralTrue = $intOrLiteralTrue; $this->intOrLiteralFalse = $intOrLiteralFalse; + $this->constantWithStringValue = $constantWithStringValue; + $this->constantWithIntegerValue = $constantWithIntegerValue; } } diff --git a/tests/Unit/Type/Parser/Lexer/Token/GenericClassNameTokenTest.php b/tests/Unit/Type/Parser/Lexer/Token/GenericClassNameTokenTest.php index bbf8008..0989bec 100644 --- a/tests/Unit/Type/Parser/Lexer/Token/GenericClassNameTokenTest.php +++ b/tests/Unit/Type/Parser/Lexer/Token/GenericClassNameTokenTest.php @@ -6,6 +6,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Parser\Lexer\Token; use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory; use CuyZ\Valinor\Tests\Fake\Type\Parser\Template\FakeTemplateParser; +use CuyZ\Valinor\Type\Parser\Lexer\Token\ClassNameToken; use CuyZ\Valinor\Type\Parser\Lexer\Token\GenericClassNameToken; use PHPUnit\Framework\TestCase; use stdClass; @@ -14,7 +15,7 @@ final class GenericClassNameTokenTest extends TestCase { public function test_symbol_is_correct(): void { - $token = new GenericClassNameToken(stdClass::class, new FakeTypeParserFactory(), new FakeTemplateParser()); + $token = new GenericClassNameToken(new ClassNameToken(stdClass::class), new FakeTypeParserFactory(), new FakeTemplateParser()); self::assertSame(stdClass::class, $token->symbol()); } diff --git a/tests/Unit/Utility/Reflection/ReflectionTest.php b/tests/Unit/Utility/Reflection/ReflectionTest.php index 9d26015..638cb88 100644 --- a/tests/Unit/Utility/Reflection/ReflectionTest.php +++ b/tests/Unit/Utility/Reflection/ReflectionTest.php @@ -7,6 +7,7 @@ namespace CuyZ\Valinor\Tests\Unit\Utility\Reflection; use Closure; use CuyZ\Valinor\Tests\Fake\FakeReflector; use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum; +use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyPromotion; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeIntersectionType; use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeUnionType; @@ -230,6 +231,12 @@ final class ReflectionTest extends TestCase '\'foo\'', ]; + yield 'phpdoc const with joker' => [ + /** @return ObjectWithConstants::CONST_WITH_* */ + fn () => ObjectWithConstants::CONST_WITH_STRING_VALUE_A, + 'ObjectWithConstants::CONST_WITH_*', + ]; + if (PHP_VERSION_ID >= 8_01_00) { yield 'phpdoc enum with joker' => [ /** @return BackedStringEnum::BA* */