mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +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(
|
||||
private int|string $simpleUnion,
|
||||
|
||||
/** @var class-string<SomeInterface>|class-string<AnotherInterface> */
|
||||
/** @var class-string<SomeInterface|AnotherInterface> */
|
||||
private string $unionOfClassString,
|
||||
|
||||
/** @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\Type;
|
||||
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use CuyZ\Valinor\Utility\IsSingleton;
|
||||
|
||||
/** @internal */
|
||||
@ -27,7 +28,7 @@ final class ClassStringToken implements TraversingToken
|
||||
|
||||
$type = $stream->read();
|
||||
|
||||
if (! $type instanceof ObjectType) {
|
||||
if (! $type instanceof ObjectType && ! $type instanceof UnionType) {
|
||||
throw new InvalidClassStringSubType($type);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ use CuyZ\Valinor\Type\StringType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
||||
use CuyZ\Valinor\Type\Types\Exception\InvalidClassString;
|
||||
use CuyZ\Valinor\Type\Types\Exception\InvalidUnionOfClassString;
|
||||
use Stringable;
|
||||
|
||||
use function class_exists;
|
||||
@ -18,12 +19,24 @@ use function is_string;
|
||||
/** @api */
|
||||
final class ClassStringType implements StringType
|
||||
{
|
||||
private ?ObjectType $subType;
|
||||
/** @var ObjectType|UnionType|null */
|
||||
private ?Type $subType;
|
||||
|
||||
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->signature = $this->subType
|
||||
? "class-string<$this->subType>"
|
||||
@ -44,7 +57,18 @@ final class ClassStringType implements StringType
|
||||
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
|
||||
@ -89,18 +113,17 @@ final class ClassStringType implements StringType
|
||||
|
||||
$value = (string)$value; // @phpstan-ignore-line
|
||||
|
||||
if (! $this->subType) {
|
||||
return $value;
|
||||
if (! $this->accepts($value)) {
|
||||
throw new InvalidClassString($value, $this->subType);
|
||||
}
|
||||
|
||||
if (is_a($value, $this->subType->className(), true)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
throw new InvalidClassString($value, $this->subType);
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function subType(): ?ObjectType
|
||||
/**
|
||||
* @return ObjectType|UnionType|null
|
||||
*/
|
||||
public function subType(): ?Type
|
||||
{
|
||||
return $this->subType;
|
||||
}
|
||||
|
@ -4,17 +4,38 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Type\Types\Exception;
|
||||
|
||||
use CuyZ\Valinor\Type\ObjectType;
|
||||
use CuyZ\Valinor\Type\Type;
|
||||
use CuyZ\Valinor\Type\Types\UnionType;
|
||||
use LogicException;
|
||||
|
||||
use function count;
|
||||
use function implode;
|
||||
|
||||
/** @api */
|
||||
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(
|
||||
"Invalid class string `$raw`, it must be a subtype of `$type`.",
|
||||
1608132562
|
||||
);
|
||||
$types = [];
|
||||
|
||||
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>',
|
||||
'type' => ClassStringType::class,
|
||||
],
|
||||
'Class string of union' => [
|
||||
'raw' => 'class-string<DateTimeInterface|stdClass>',
|
||||
'transformed' => 'class-string<DateTimeInterface|stdClass>',
|
||||
'type' => ClassStringType::class,
|
||||
],
|
||||
'Class name' => [
|
||||
'raw' => 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\FakeType;
|
||||
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\ClassStringType;
|
||||
use CuyZ\Valinor\Type\Types\Exception\CannotCastValue;
|
||||
@ -22,13 +23,31 @@ use stdClass;
|
||||
|
||||
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();
|
||||
|
||||
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
|
||||
{
|
||||
$classStringType = new ClassStringType();
|
||||
@ -62,6 +81,26 @@ final class ClassStringTypeTest extends TestCase
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
$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();
|
||||
$classStringObject = new StringableObject(DateTimeInterface::class);
|
||||
@ -122,6 +172,18 @@ final class ClassStringTypeTest extends TestCase
|
||||
(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
|
||||
{
|
||||
$objectType = new FakeObjectType();
|
||||
|
Loading…
Reference in New Issue
Block a user