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(
private int|string $simpleUnion,
/** @var class-string<SomeInterface>|class-string<AnotherInterface> */
/** @var class-string<SomeInterface|AnotherInterface> */
private string $unionOfClassString,
/** @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\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);
}

View File

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

View File

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

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

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