Skip to content

Commit 7d2912c

Browse files
author
Kirill Nesmeyanov
committed
Add class subtype normalization
1 parent d4dee72 commit 7d2912c

File tree

6 files changed

+95
-28
lines changed

6 files changed

+95
-28
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\Mapper\Exception\Mapping;
6+
7+
use TypeLang\Mapper\Runtime\Context;
8+
use TypeLang\Mapper\Runtime\Path\PathInterface;
9+
use TypeLang\Parser\Node\Stmt\TypeStatement;
10+
11+
class NonInstantiatableObjectException extends ObjectException
12+
{
13+
/**
14+
* @param \ReflectionClass<object> $value
15+
*/
16+
public function __construct(
17+
TypeStatement $expected,
18+
\ReflectionClass $value,
19+
PathInterface $path,
20+
string $template,
21+
int $code = 0,
22+
?\Throwable $previous = null,
23+
) {
24+
parent::__construct($expected, $value, $path, $template, $code, $previous);
25+
}
26+
27+
/**
28+
* @param \ReflectionClass<object> $value
29+
*/
30+
public static function createFromPath(
31+
?TypeStatement $expected,
32+
\ReflectionClass $value,
33+
PathInterface $path,
34+
?\Throwable $previous = null,
35+
): self {
36+
$template = \sprintf('Unable to instantiate %s of {{expected}}', match (true) {
37+
$value->isAbstract() => 'abstract class',
38+
$value->isInterface() => 'interface',
39+
default => 'unknown non-instantiable type',
40+
});
41+
42+
return new self(
43+
expected: $expected ?? self::mixedTypeStatement(),
44+
value: $value,
45+
path: $path,
46+
template: $template,
47+
previous: $previous,
48+
);
49+
}
50+
51+
/**
52+
* @param \ReflectionClass<object> $value
53+
*/
54+
public static function createFromContext(
55+
?TypeStatement $expected,
56+
\ReflectionClass $value,
57+
Context $context,
58+
?\Throwable $previous = null,
59+
): self {
60+
return self::createFromPath(
61+
expected: $expected,
62+
value: $value,
63+
path: $context->getPath(),
64+
previous: $previous,
65+
);
66+
}
67+
}

src/Type/Builder/ClassTypeBuilder.php

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -68,32 +68,12 @@ public function build(
6868
/** @var class-string<T> $class */
6969
$class = $statement->name->toString();
7070

71-
$reflection = new \ReflectionClass($class);
72-
73-
$metadata = $this->driver->getClassMetadata(
74-
class: $reflection,
75-
types: $types,
76-
parser: $parser,
77-
);
78-
79-
$discriminator = $metadata->findDiscriminator();
80-
81-
if ($discriminator === null && !$reflection->isInstantiable()) {
82-
throw InternalTypeException::becauseInternalTypeErrorOccurs(
83-
type: $statement,
84-
message: \vsprintf('%s "%s" expects a discriminator map to be able to be created', [
85-
match (true) {
86-
$reflection->isAbstract() => 'Non-creatable abstract class',
87-
$reflection->isInterface() => 'Non-creatable interface',
88-
default => 'Unknown non-instantiable type',
89-
},
90-
$reflection->getName(),
91-
]),
92-
);
93-
}
94-
9571
return new ClassType(
96-
metadata: $metadata,
72+
metadata: $this->driver->getClassMetadata(
73+
class: new \ReflectionClass($class),
74+
types: $types,
75+
parser: $parser,
76+
),
9777
accessor: $this->accessor,
9878
instantiator: $this->instantiator,
9979
);

src/Type/ClassType/ClassInstantiator/ClassInstantiatorInterface.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace TypeLang\Mapper\Type\ClassType\ClassInstantiator;
66

7+
use TypeLang\Mapper\Exception\Mapping\NonInstantiatableObjectException;
78
use TypeLang\Mapper\Mapping\Metadata\ClassMetadata;
9+
use TypeLang\Mapper\Runtime\Context;
810

911
interface ClassInstantiatorInterface
1012
{
@@ -14,7 +16,8 @@ interface ClassInstantiatorInterface
1416
* @param ClassMetadata<T> $class
1517
*
1618
* @return T
19+
* @throws NonInstantiatableObjectException occurs in case of object is not instantiatable
1720
* @throws \Throwable occurs for some reason when creating an object
1821
*/
19-
public function instantiate(ClassMetadata $class): object;
22+
public function instantiate(ClassMetadata $class, Context $context): object;
2023
}

src/Type/ClassType/ClassInstantiator/ReflectionClassInstantiator.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@
44

55
namespace TypeLang\Mapper\Type\ClassType\ClassInstantiator;
66

7+
use TypeLang\Mapper\Exception\Mapping\NonInstantiatableObjectException;
78
use TypeLang\Mapper\Mapping\Metadata\ClassMetadata;
9+
use TypeLang\Mapper\Runtime\Context;
810

911
final class ReflectionClassInstantiator implements ClassInstantiatorInterface
1012
{
11-
public function instantiate(ClassMetadata $class): object
13+
public function instantiate(ClassMetadata $class, Context $context): object
1214
{
1315
$reflection = new \ReflectionClass($class->getName());
1416

17+
if (!$reflection->isInstantiable()) {
18+
throw NonInstantiatableObjectException::createFromContext(
19+
expected: $class->getTypeStatement($context),
20+
value: $reflection,
21+
context: $context,
22+
);
23+
}
24+
1525
return $reflection->newInstanceWithoutConstructor();
1626
}
1727
}

src/Type/ClassType/ClassTypeDenormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function cast(mixed $value, Context $context): mixed
6565

6666
$entrance = $context->enter($value, new ObjectEntry($this->metadata->getName()));
6767

68-
$instance = $this->instantiator->instantiate($this->metadata);
68+
$instance = $this->instantiator->instantiate($this->metadata, $context);
6969

7070
$this->denormalizeObject($value, $instance, $entrance);
7171

src/Type/ClassType/ClassTypeNormalizer.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ public function cast(mixed $value, Context $context): object|array
5151
);
5252
}
5353

54+
// Subtype normalization
55+
if ($value::class !== $className) {
56+
/** @var object|array<non-empty-string, mixed> */
57+
return $context->getTypeByValue($value)
58+
->cast($value, $context);
59+
}
60+
5461
$entrance = $context->enter($value, new ObjectEntry($this->metadata->getName()));
5562

5663
$result = $this->normalizeObject($value, $entrance);

0 commit comments

Comments
 (0)