mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
feat: handle class string of union of object
This commit is contained in:
parent
fdb8154368
commit
b7923bc383
@ -776,7 +776,7 @@ final class SomeClass
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private int|string $simpleUnion,
|
private int|string $simpleUnion,
|
||||||
|
|
||||||
/** @var class-string<SomeInterface>|class-string<AnotherInterface> */
|
/** @var class-string<SomeInterface|AnotherInterface> */
|
||||||
private string $unionOfClassString,
|
private string $unionOfClassString,
|
||||||
|
|
||||||
/** @var array<SomeInterface|AnotherInterface> */
|
/** @var array<SomeInterface|AnotherInterface> */
|
||||||
|
@ -10,6 +10,7 @@ use CuyZ\Valinor\Type\Parser\Exception\Scalar\InvalidClassStringSubType;
|
|||||||
use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
|
use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
|
||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Type;
|
||||||
use CuyZ\Valinor\Type\Types\ClassStringType;
|
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||||
|
use CuyZ\Valinor\Type\Types\UnionType;
|
||||||
use CuyZ\Valinor\Utility\IsSingleton;
|
use CuyZ\Valinor\Utility\IsSingleton;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -27,7 +28,7 @@ final class ClassStringToken implements TraversingToken
|
|||||||
|
|
||||||
$type = $stream->read();
|
$type = $stream->read();
|
||||||
|
|
||||||
if (! $type instanceof ObjectType) {
|
if (! $type instanceof ObjectType && ! $type instanceof UnionType) {
|
||||||
throw new InvalidClassStringSubType($type);
|
throw new InvalidClassStringSubType($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ use CuyZ\Valinor\Type\StringType;
|
|||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Type;
|
||||||
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
||||||
use CuyZ\Valinor\Type\Types\Exception\InvalidClassString;
|
use CuyZ\Valinor\Type\Types\Exception\InvalidClassString;
|
||||||
|
use CuyZ\Valinor\Type\Types\Exception\InvalidUnionOfClassString;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
use function class_exists;
|
use function class_exists;
|
||||||
@ -18,12 +19,24 @@ use function is_string;
|
|||||||
/** @api */
|
/** @api */
|
||||||
final class ClassStringType implements StringType
|
final class ClassStringType implements StringType
|
||||||
{
|
{
|
||||||
private ?ObjectType $subType;
|
/** @var ObjectType|UnionType|null */
|
||||||
|
private ?Type $subType;
|
||||||
|
|
||||||
private string $signature;
|
private string $signature;
|
||||||
|
|
||||||
public function __construct(ObjectType $subType = null)
|
/**
|
||||||
|
* @param ObjectType|UnionType $subType
|
||||||
|
*/
|
||||||
|
public function __construct(Type $subType = null)
|
||||||
{
|
{
|
||||||
|
if ($subType instanceof UnionType) {
|
||||||
|
foreach ($subType->types() as $type) {
|
||||||
|
if (! $type instanceof ObjectType) {
|
||||||
|
throw new InvalidUnionOfClassString($subType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->subType = $subType;
|
$this->subType = $subType;
|
||||||
$this->signature = $this->subType
|
$this->signature = $this->subType
|
||||||
? "class-string<$this->subType>"
|
? "class-string<$this->subType>"
|
||||||
@ -44,7 +57,18 @@ final class ClassStringType implements StringType
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return is_a($value, $this->subType->className(), true);
|
if ($this->subType instanceof ObjectType) {
|
||||||
|
return is_a($value, $this->subType->className(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->subType->types() as $type) {
|
||||||
|
/** @var ObjectType $type */
|
||||||
|
if (is_a($value, $type->className(), true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function matches(Type $other): bool
|
public function matches(Type $other): bool
|
||||||
@ -89,18 +113,17 @@ final class ClassStringType implements StringType
|
|||||||
|
|
||||||
$value = (string)$value; // @phpstan-ignore-line
|
$value = (string)$value; // @phpstan-ignore-line
|
||||||
|
|
||||||
if (! $this->subType) {
|
if (! $this->accepts($value)) {
|
||||||
return $value;
|
throw new InvalidClassString($value, $this->subType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_a($value, $this->subType->className(), true)) {
|
return $value;
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidClassString($value, $this->subType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subType(): ?ObjectType
|
/**
|
||||||
|
* @return ObjectType|UnionType|null
|
||||||
|
*/
|
||||||
|
public function subType(): ?Type
|
||||||
{
|
{
|
||||||
return $this->subType;
|
return $this->subType;
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,38 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace CuyZ\Valinor\Type\Types\Exception;
|
namespace CuyZ\Valinor\Type\Types\Exception;
|
||||||
|
|
||||||
|
use CuyZ\Valinor\Type\ObjectType;
|
||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Type;
|
||||||
|
use CuyZ\Valinor\Type\Types\UnionType;
|
||||||
use LogicException;
|
use LogicException;
|
||||||
|
|
||||||
|
use function count;
|
||||||
|
use function implode;
|
||||||
|
|
||||||
/** @api */
|
/** @api */
|
||||||
final class InvalidClassString extends LogicException implements CastError
|
final class InvalidClassString extends LogicException implements CastError
|
||||||
{
|
{
|
||||||
public function __construct(string $raw, Type $type)
|
/**
|
||||||
|
* @param ObjectType|UnionType|null $type
|
||||||
|
*/
|
||||||
|
public function __construct(string $raw, ?Type $type)
|
||||||
{
|
{
|
||||||
parent::__construct(
|
$types = [];
|
||||||
"Invalid class string `$raw`, it must be a subtype of `$type`.",
|
|
||||||
1608132562
|
if ($type instanceof ObjectType) {
|
||||||
);
|
$types = [$type];
|
||||||
|
} elseif ($type instanceof UnionType) {
|
||||||
|
$types = $type->types();
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = "Invalid class string `$raw`.";
|
||||||
|
|
||||||
|
if (count($types) > 1) {
|
||||||
|
$message = "Invalid class string `$raw`, it must be one of `" . implode('`, `', $types) . "`.";
|
||||||
|
} elseif (count($types) === 1) {
|
||||||
|
$message = "Invalid class string `$raw`, it must be a subtype of `$type`.";
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($message, 1608132562);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
src/Type/Types/Exception/InvalidUnionOfClassString.php
Normal file
21
src/Type/Types/Exception/InvalidUnionOfClassString.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace CuyZ\Valinor\Type\Types\Exception;
|
||||||
|
|
||||||
|
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
|
||||||
|
use CuyZ\Valinor\Type\Types\UnionType;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
final class InvalidUnionOfClassString extends LogicException implements InvalidType
|
||||||
|
{
|
||||||
|
public function __construct(UnionType $type)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
"Type `$type` contains invalid class string element(s).",
|
||||||
|
1648830951
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -425,6 +425,11 @@ final class NativeLexerTest extends TestCase
|
|||||||
'transformed' => 'class-string<DateTimeInterface>',
|
'transformed' => 'class-string<DateTimeInterface>',
|
||||||
'type' => ClassStringType::class,
|
'type' => ClassStringType::class,
|
||||||
],
|
],
|
||||||
|
'Class string of union' => [
|
||||||
|
'raw' => 'class-string<DateTimeInterface|stdClass>',
|
||||||
|
'transformed' => 'class-string<DateTimeInterface|stdClass>',
|
||||||
|
'type' => ClassStringType::class,
|
||||||
|
],
|
||||||
'Class name' => [
|
'Class name' => [
|
||||||
'raw' => stdClass::class,
|
'raw' => stdClass::class,
|
||||||
'transformed' => stdClass::class,
|
'transformed' => stdClass::class,
|
||||||
|
@ -7,6 +7,7 @@ namespace CuyZ\Valinor\Tests\Unit\Type\Types;
|
|||||||
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
|
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
|
||||||
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\StringableObject;
|
use CuyZ\Valinor\Tests\Fixture\Object\StringableObject;
|
||||||
|
use CuyZ\Valinor\Type\Types\Exception\InvalidUnionOfClassString;
|
||||||
use CuyZ\Valinor\Type\Types\NativeStringType;
|
use CuyZ\Valinor\Type\Types\NativeStringType;
|
||||||
use CuyZ\Valinor\Type\Types\ClassStringType;
|
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||||
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
||||||
@ -22,13 +23,31 @@ use stdClass;
|
|||||||
|
|
||||||
final class ClassStringTypeTest extends TestCase
|
final class ClassStringTypeTest extends TestCase
|
||||||
{
|
{
|
||||||
public function test_subtype_can_be_retrieved(): void
|
public function test_string_subtype_can_be_retrieved(): void
|
||||||
{
|
{
|
||||||
$subType = new FakeObjectType();
|
$subType = new FakeObjectType();
|
||||||
|
|
||||||
self::assertSame($subType, (new ClassStringType($subType))->subType());
|
self::assertSame($subType, (new ClassStringType($subType))->subType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_union_of_string_subtype_can_be_retrieved(): void
|
||||||
|
{
|
||||||
|
$subType = new UnionType(new FakeObjectType(), new FakeObjectType());
|
||||||
|
|
||||||
|
self::assertSame($subType, (new ClassStringType($subType))->subType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_union_with_invalid_type_throws_exception(): void
|
||||||
|
{
|
||||||
|
$type = new UnionType(new FakeObjectType(), new FakeType());
|
||||||
|
|
||||||
|
$this->expectException(InvalidUnionOfClassString::class);
|
||||||
|
$this->expectExceptionCode(1648830951);
|
||||||
|
$this->expectExceptionMessage("Type `$type` contains invalid class string element(s).");
|
||||||
|
|
||||||
|
new ClassStringType($type);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_accepts_correct_values(): void
|
public function test_accepts_correct_values(): void
|
||||||
{
|
{
|
||||||
$classStringType = new ClassStringType();
|
$classStringType = new ClassStringType();
|
||||||
@ -62,6 +81,26 @@ final class ClassStringTypeTest extends TestCase
|
|||||||
self::assertFalse($classStringType->accepts(stdClass::class));
|
self::assertFalse($classStringType->accepts(stdClass::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_accepts_correct_values_with_union_sub_type(): void
|
||||||
|
{
|
||||||
|
$type = new UnionType(new FakeObjectType(DateTimeInterface::class), new FakeObjectType(stdClass::class));
|
||||||
|
$classStringType = new ClassStringType($type);
|
||||||
|
|
||||||
|
self::assertTrue($classStringType->accepts(DateTime::class));
|
||||||
|
self::assertTrue($classStringType->accepts(DateTimeImmutable::class));
|
||||||
|
self::assertTrue($classStringType->accepts(DateTimeInterface::class));
|
||||||
|
|
||||||
|
self::assertTrue($classStringType->accepts(stdClass::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_does_not_accept_incorrect_values_with_union_sub_type(): void
|
||||||
|
{
|
||||||
|
$unionType = new UnionType(new FakeObjectType(DateTime::class), new FakeObjectType(stdClass::class));
|
||||||
|
$classStringType = new ClassStringType($unionType);
|
||||||
|
|
||||||
|
self::assertFalse($classStringType->accepts(DateTimeImmutable::class));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_can_cast_stringable_value(): void
|
public function test_can_cast_stringable_value(): void
|
||||||
{
|
{
|
||||||
self::assertTrue((new ClassStringType())->canCast('foo'));
|
self::assertTrue((new ClassStringType())->canCast('foo'));
|
||||||
@ -111,6 +150,17 @@ final class ClassStringTypeTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function test_cast_invalid_class_string_throws_exception(): void
|
public function test_cast_invalid_class_string_throws_exception(): void
|
||||||
|
{
|
||||||
|
$classStringObject = new StringableObject('foo');
|
||||||
|
|
||||||
|
$this->expectException(InvalidClassString::class);
|
||||||
|
$this->expectExceptionCode(1608132562);
|
||||||
|
$this->expectExceptionMessage("Invalid class string `foo`.");
|
||||||
|
|
||||||
|
(new ClassStringType())->cast($classStringObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_cast_invalid_class_string_of_object_type_throws_exception(): void
|
||||||
{
|
{
|
||||||
$objectType = new FakeObjectType();
|
$objectType = new FakeObjectType();
|
||||||
$classStringObject = new StringableObject(DateTimeInterface::class);
|
$classStringObject = new StringableObject(DateTimeInterface::class);
|
||||||
@ -122,6 +172,18 @@ final class ClassStringTypeTest extends TestCase
|
|||||||
(new ClassStringType($objectType))->cast($classStringObject);
|
(new ClassStringType($objectType))->cast($classStringObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_cast_invalid_class_string_of_union_type_throws_exception(): void
|
||||||
|
{
|
||||||
|
$unionType = new UnionType(new FakeObjectType(DateTime::class), new FakeObjectType(stdClass::class));
|
||||||
|
$classStringObject = new StringableObject(DateTimeInterface::class);
|
||||||
|
|
||||||
|
$this->expectException(InvalidClassString::class);
|
||||||
|
$this->expectExceptionCode(1608132562);
|
||||||
|
$this->expectExceptionMessage("Invalid class string `DateTimeInterface`, it must be one of `DateTime`, `stdClass`.");
|
||||||
|
|
||||||
|
(new ClassStringType($unionType))->cast($classStringObject);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_string_value_is_correct(): void
|
public function test_string_value_is_correct(): void
|
||||||
{
|
{
|
||||||
$objectType = new FakeObjectType();
|
$objectType = new FakeObjectType();
|
||||||
|
Loading…
Reference in New Issue
Block a user