feat: handle class string of union of object

This commit is contained in:
Romain Canon 2022-04-04 13:01:39 +02:00
parent fdb8154368
commit b7923bc383
7 changed files with 152 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View 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
);
}
}

View File

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

View File

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