feat: add support for class constant type

This notation is mainly useful when several cases in constants of a
class share a common prefix.

```php
final class SomeClassWithConstants
{
    public const FOO = 1337;

    public const BAR = 'bar';

    public const BAZ = 'baz';
}

$mapper = (new MapperBuilder())->mapper();

$mapper->map('SomeClassWithConstants::BA*', 1337); // error
$mapper->map('SomeClassWithConstants::BA*', 'bar'); // ok
$mapper->map('SomeClassWithConstants::BA*', 'baz'); // ok
```
This commit is contained in:
Romain Canon 2022-10-04 22:10:17 +02:00
parent 37f96f101d
commit 1244c2d68f
17 changed files with 660 additions and 49 deletions

View File

@ -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

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type\Parser\Exception\Constant;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;
/** @internal */
final class ClassConstantCaseNotFound extends RuntimeException implements InvalidType
{
/**
* @param class-string $className
*/
public function __construct(string $className, string $case)
{
$message = strpos($case, '*') !== false
? "Cannot find class constant case with pattern `$className::$case`."
: "Unknown class constant case `$className::$case`.";
parent::__construct($message, 1652189140);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type\Parser\Exception\Constant;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;
/** @internal */
final class MissingClassConstantCase extends RuntimeException implements InvalidType
{
/**
* @param class-string $className
*/
public function __construct(string $className)
{
parent::__construct(
"Missing case name for class constant `$className::?`.",
1664905018
);
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type\Parser\Exception\Constant;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;
/** @internal */
final class MissingClassConstantColon extends RuntimeException implements InvalidType
{
/**
* @param class-string $className
*/
public function __construct(string $className, string $case)
{
if ($case === ':') {
$case = '?';
}
parent::__construct(
"Missing second colon symbol for class constant `$className::$case`.",
1652189143
);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Type\Parser\Exception\Constant;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use RuntimeException;
/** @internal */
final class MissingSpecificClassConstantCase extends RuntimeException implements InvalidType
{
/**
* @param class-string $className
*/
public function __construct(string $className)
{
parent::__construct(
"Missing specific case for class constant `$className::?` (cannot be `*`).",
1664904636
);
}
}

View File

@ -31,9 +31,7 @@ final class ClassGenericLexer implements TypeLexer
$token = $this->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;

View File

@ -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<object> */
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<string, mixed>
*/
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;
}
}

View File

@ -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<string, Type> $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<string, Type> $templates
* @param Type[] $generics
* @return array<string, Type>
*/
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;

View File

@ -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 {

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fixture\Object;
class ObjectWithConstants
{
public const CONST_WITH_STRING_VALUE_A = 'some string value';
public const CONST_WITH_STRING_VALUE_B = 'another string value';
private const CONST_WITH_STRING_PRIVATE_VALUE = 'some private string value'; // @phpstan-ignore-line
public const CONST_WITH_PREFIX_WITH_STRING_VALUE = 'some prefixed string value';
public const CONST_WITH_INTEGER_VALUE_A = 1653398288;
public const CONST_WITH_INTEGER_VALUE_B = 1653398289;
public const CONST_WITH_FLOAT_VALUE_A = 1337.42;
public const CONST_WITH_FLOAT_VALUE_B = 404.512;
public const CONST_WITH_ARRAY_VALUE_A = [
'string' => '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<self>
*/
public static function className(): string
{
return PHP_VERSION_ID >= 8_01_00
? ObjectWithConstantsIncludingEnums::class
: ObjectWithConstants::class;
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fixture\Object;
use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
final class ObjectWithConstantsIncludingEnums extends ObjectWithConstants
{
public const CONST_WITH_ENUM_VALUE_A = BackedIntegerEnum::FOO;
public const CONST_WITH_ENUM_VALUE_B = BackedIntegerEnum::BAR;
}

View File

@ -8,7 +8,12 @@ use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum;
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
use CuyZ\Valinor\Tests\Fixture\Object\AbstractObject;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
use CuyZ\Valinor\Type\IntegerType;
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\Exception\Enum\EnumCaseNotFound;
use CuyZ\Valinor\Type\Parser\Exception\Enum\MissingEnumCase;
use CuyZ\Valinor\Type\Parser\Exception\Enum\MissingEnumColon;
@ -865,6 +870,44 @@ final class NativeLexerTest extends TestCase
'type' => 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');
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping\Object;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstantsIncludingEnums;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
final class ConstantValuesMappingTest extends IntegrationTest
{
public function test_values_are_mapped_properly(): void
{
$source = [
'constantStringValue' => '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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}

View File

@ -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* */