mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 12:14:40 +01:00
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:
parent
37f96f101d
commit
1244c2d68f
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
63
tests/Fixture/Object/ObjectWithConstants.php
Normal file
63
tests/Fixture/Object/ObjectWithConstants.php
Normal 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;
|
||||
}
|
||||
}
|
14
tests/Fixture/Object/ObjectWithConstantsIncludingEnums.php
Normal file
14
tests/Fixture/Object/ObjectWithConstantsIncludingEnums.php
Normal 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;
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
180
tests/Integration/Mapping/Object/ConstantValuesMappingTest.php
Normal file
180
tests/Integration/Mapping/Object/ConstantValuesMappingTest.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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* */
|
||||
|
Loading…
Reference in New Issue
Block a user