Skip to content

Commit 78bdc0c

Browse files
committed
Add asymmetric types and hooks support for reflection driver
1 parent 39538f9 commit 78bdc0c

File tree

9 files changed

+122
-34
lines changed

9 files changed

+122
-34
lines changed

src/Mapping/Driver/ArrayConfigDriver/TypePropertyConfigLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function load(
2727
// @phpstan-ignore-next-line : Additional DbC invariant
2828
assert(\is_string($config['type']));
2929

30-
$metadata->type = $this->createPropertyType(
30+
$metadata->read = $metadata->write = $this->createPropertyType(
3131
type: $config['type'],
3232
property: $property,
3333
types: $types,

src/Mapping/Driver/AttributeDriver/TypePropertyMetadataLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function load(
2929
return;
3030
}
3131

32-
$metadata->type = $this->createPropertyType(
32+
$metadata->read = $metadata->write = $this->createPropertyType(
3333
type: $attribute->type,
3434
property: $property,
3535
types: $types,

src/Mapping/Driver/DocBlockDriver/TypePropertyDocBlockLoader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class: $reflection->name,
4646
);
4747
}
4848

49-
$metadata->type = new TypeMetadata(
49+
$metadata->read = $metadata->write = new TypeMetadata(
5050
type: $type,
5151
statement: $statement,
5252
);

src/Mapping/Driver/ReflectionDriver.php

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ protected function load(
3636

3737
$metadata = $class->getPropertyOrCreate($property->getName());
3838

39-
$this->fillType($property, $metadata, $types);
39+
$this->fillReadType($property, $metadata, $types);
40+
$this->fillWriteType($property, $metadata, $types);
41+
4042
$this->fillDefaultValue($property, $metadata);
4143
}
4244

@@ -90,27 +92,63 @@ private function fillDefaultValue(\ReflectionProperty $property, PropertyMetadat
9092
* @throws \InvalidArgumentException
9193
* @throws \Throwable
9294
*/
93-
private function fillType(
95+
private function fillReadType(
9496
\ReflectionProperty $property,
9597
PropertyMetadata $meta,
9698
TypeRepositoryInterface $types,
9799
): void {
98-
$statement = $this->getTypeStatement($property);
100+
$statement = $this->getReadTypeStatement($property);
101+
102+
$meta->read = $meta->write = $this->getTypeMetadataByStatement(
103+
statement: $statement,
104+
property: $property,
105+
types: $types,
106+
);
107+
}
108+
109+
/**
110+
* @throws \Throwable
111+
*/
112+
private function fillWriteType(
113+
\ReflectionProperty $property,
114+
PropertyMetadata $meta,
115+
TypeRepositoryInterface $types,
116+
): void {
117+
$statement = $this->findWriteTypeStatement($property);
118+
119+
if ($statement === null) {
120+
return;
121+
}
122+
123+
$meta->write = $this->getTypeMetadataByStatement(
124+
statement: $statement,
125+
property: $property,
126+
types: $types,
127+
);
128+
}
99129

130+
/**
131+
* @throws \Throwable
132+
*/
133+
private function getTypeMetadataByStatement(
134+
TypeStatement $statement,
135+
\ReflectionProperty $property,
136+
TypeRepositoryInterface $types,
137+
): TypeMetadata {
100138
try {
101139
$type = $types->getTypeByStatement($statement);
102140
} catch (TypeNotFoundException $e) {
103141
$class = $property->getDeclaringClass();
104142

105143
throw PropertyTypeNotFoundException::becauseTypeOfPropertyNotDefined(
106-
class: $class->getName(),
107-
property: $property->getName(),
144+
class: $class->name,
145+
property: $property->name,
108146
type: $e->getType(),
109147
previous: $e,
110148
);
111149
}
112150

113-
$meta->type = new TypeMetadata(
151+
return new TypeMetadata(
114152
type: $type,
115153
statement: $statement,
116154
);
@@ -119,7 +157,7 @@ class: $class->getName(),
119157
/**
120158
* @throws \InvalidArgumentException
121159
*/
122-
private function getTypeStatement(\ReflectionProperty $property): TypeStatement
160+
private function getReadTypeStatement(\ReflectionProperty $property): TypeStatement
123161
{
124162
$type = $property->getType();
125163

@@ -130,6 +168,32 @@ private function getTypeStatement(\ReflectionProperty $property): TypeStatement
130168
return $this->createTypeStatement($type);
131169
}
132170

171+
private function findWriteTypeStatement(\ReflectionProperty $property): ?TypeStatement
172+
{
173+
// Only PHP 8.4+ supports different get/set
174+
if (\PHP_VERSION_ID < 80400) {
175+
return null;
176+
}
177+
178+
$hook = $property->getHook(\PropertyHookType::Set);
179+
180+
if ($hook === null || $hook->getNumberOfParameters() < 1) {
181+
return null;
182+
}
183+
184+
foreach ($hook->getParameters() as $parameter) {
185+
$type = $parameter->getType();
186+
187+
if ($type === null) {
188+
return null;
189+
}
190+
191+
return $this->createTypeStatement($type);
192+
}
193+
194+
return null;
195+
}
196+
133197
private static function isValidProperty(\ReflectionProperty $property): bool
134198
{
135199
return !$property->isStatic()

src/Mapping/Metadata/ClassMetadata.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public function __construct(
7575
*
7676
* @codeCoverageIgnore
7777
*/
78-
public function getTypeStatement(Context $context): TypeStatement
78+
public function getTypeStatement(Context $context, bool $read): TypeStatement
7979
{
8080
if (!$context->isDetailedTypes()) {
8181
return new NamedTypeNode($this->name);
@@ -84,7 +84,7 @@ public function getTypeStatement(Context $context): TypeStatement
8484
$fields = [];
8585

8686
foreach ($this->getProperties() as $property) {
87-
$field = $property->getFieldNode($context);
87+
$field = $property->getFieldNode($context, $read);
8888

8989
if ($field === null) {
9090
continue;

src/Mapping/Metadata/PropertyMetadata.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,28 @@ final class PropertyMetadata extends Metadata
4343
*/
4444
private array $skipWhen = [];
4545

46+
/**
47+
* Gets property type information for reading
48+
*/
49+
public ?TypeMetadata $read = null;
50+
51+
/**
52+
* Gets property type information for writing
53+
*/
54+
public ?TypeMetadata $write = null;
55+
4656
public function __construct(
4757
/**
4858
* Gets property real name.
4959
*
5060
* @var non-empty-string
5161
*/
5262
public readonly string $name,
53-
/**
54-
* Gets property type info.
55-
*/
56-
public ?TypeMetadata $type = null,
63+
?TypeMetadata $type = null,
5764
?int $createdAt = null,
5865
) {
5966
$this->alias = $this->name;
67+
$this->read = $this->write = $type;
6068

6169
parent::__construct($createdAt);
6270
}
@@ -66,9 +74,9 @@ public function __construct(
6674
*
6775
* @codeCoverageIgnore
6876
*/
69-
public function getTypeStatement(Context $context): ?TypeStatement
77+
public function getTypeStatement(Context $context, bool $read): ?TypeStatement
7078
{
71-
$info = $this->type;
79+
$info = $read ? $this->read : $this->write;
7280

7381
if ($info === null) {
7482
return null;
@@ -88,9 +96,9 @@ public function getTypeStatement(Context $context): ?TypeStatement
8896
*
8997
* @codeCoverageIgnore
9098
*/
91-
public function getFieldNode(Context $context): ?NamedFieldNode
99+
public function getFieldNode(Context $context, bool $read): ?NamedFieldNode
92100
{
93-
$statement = $this->getTypeStatement($context);
101+
$statement = $this->getTypeStatement($context, $read);
94102

95103
if ($statement === null) {
96104
return null;

src/Type/ClassType/ClassTypeDenormalizer.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function match(mixed $value, Context $context): bool
4949
private function getPropertyType(PropertyMetadata $meta, Context $context): TypeInterface
5050
{
5151
// Fetch field type
52-
$info = $meta->type;
52+
$info = $meta->write;
5353

5454
if ($info === null) {
5555
return $context->getTypeByStatement(new NamedTypeNode('mixed'));
@@ -105,7 +105,7 @@ public function cast(mixed $value, Context $context): mixed
105105

106106
if (!\is_array($value)) {
107107
throw InvalidValueOfTypeException::createFromContext(
108-
expected: $this->metadata->getTypeStatement($context),
108+
expected: $this->metadata->getTypeStatement($context, read: false),
109109
value: $value,
110110
context: $context,
111111
);
@@ -123,7 +123,7 @@ public function cast(mixed $value, Context $context): mixed
123123
$instance = $this->instantiator->instantiate($this->metadata->name);
124124
} catch (\Throwable $e) {
125125
throw NonInstantiatableException::createFromContext(
126-
expected: $this->metadata->getTypeStatement($context),
126+
expected: $this->metadata->getTypeStatement($context, read: false),
127127
class: $this->metadata->name,
128128
context: $context,
129129
previous: $e,
@@ -166,7 +166,7 @@ private function denormalizeObject(array $value, object $object, Context $contex
166166
$exception = InvalidObjectValueException::createFromContext(
167167
element: $element,
168168
field: $meta->alias,
169-
expected: $meta->getTypeStatement($entrance),
169+
expected: $meta->getTypeStatement($entrance, read: false),
170170
value: (object) $value,
171171
context: $entrance,
172172
previous: $e,
@@ -188,7 +188,7 @@ private function denormalizeObject(array $value, object $object, Context $contex
188188
default:
189189
$exception = MissingRequiredObjectFieldException::createFromContext(
190190
field: $meta->alias,
191-
expected: $meta->getTypeStatement($entrance),
191+
expected: $meta->getTypeStatement($entrance, read: false),
192192
value: (object) $value,
193193
context: $entrance,
194194
);

src/Type/ClassType/ClassTypeNormalizer.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function cast(mixed $value, Context $context): object|array
4545

4646
if (!$value instanceof $className) {
4747
throw InvalidValueOfTypeException::createFromContext(
48-
expected: $this->metadata->getTypeStatement($context),
48+
expected: $this->metadata->getTypeStatement($context, read: true),
4949
value: $value,
5050
context: $context,
5151
);
@@ -98,7 +98,7 @@ protected function normalizeObject(object $object, Context $context): array
9898
}
9999

100100
// Fetch field type
101-
$info = $meta->type;
101+
$info = $meta->read;
102102
$type = $info !== null ? $info->type : $context->getTypeByDefinition('mixed');
103103

104104
try {
@@ -110,7 +110,7 @@ protected function normalizeObject(object $object, Context $context): array
110110
$exception = InvalidObjectValueException::createFromContext(
111111
element: $element,
112112
field: $meta->alias,
113-
expected: $meta->getTypeStatement($entrance),
113+
expected: $meta->getTypeStatement($entrance, read: true),
114114
value: $object,
115115
context: $entrance,
116116
previous: $e,

tests/Mapping/Metadata/PropertyMetadataTest.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,36 @@ public function testNames(): void
2424
self::assertSame('export', $m->name);
2525
}
2626

27-
public function testTypeInfo(): void
27+
public function testReadTypeInfo(): void
2828
{
2929
$type = $this->createMock(TypeInterface::class);
3030
$stmt = new NamedTypeNode('int');
3131
$tm = new TypeMetadata($type, $stmt);
3232

3333
$m = new PropertyMetadata('a');
34-
self::assertNull($m->type);
34+
self::assertNull($m->read);
3535

36-
$m->type = $tm;
37-
self::assertSame($tm, $m->type);
36+
$m->read = $tm;
37+
self::assertSame($tm, $m->read);
3838

39-
$m->type = null;
40-
self::assertNull($m->type);
39+
$m->read = null;
40+
self::assertNull($m->read);
41+
}
42+
43+
public function testWriteTypeInfo(): void
44+
{
45+
$type = $this->createMock(TypeInterface::class);
46+
$stmt = new NamedTypeNode('int');
47+
$tm = new TypeMetadata($type, $stmt);
48+
49+
$m = new PropertyMetadata('a');
50+
self::assertNull($m->write);
51+
52+
$m->write = $tm;
53+
self::assertSame($tm, $m->write);
54+
55+
$m->write = null;
56+
self::assertNull($m->write);
4157
}
4258

4359
public function testSkipConditions(): void

0 commit comments

Comments
 (0)