Skip to content

Commit 77a2937

Browse files
committed
better deserializers split
1 parent 997f92c commit 77a2937

File tree

6 files changed

+368
-239
lines changed

6 files changed

+368
-239
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@
169169
},
170170
"config": {
171171
"allow-plugins": {
172-
"symfony/runtime": true
172+
"symfony/runtime": true,
173+
"php-http/discovery": false
173174
}
174175
},
175176
"autoload": {

src/Symfony/Component/SerDes/Internal/Deserialize/Deserializer.php

Lines changed: 142 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
* @author Mathias Arlaud <[email protected]>
2424
*
2525
* @internal
26+
*
27+
* @template T of mixed
2628
*/
27-
class Deserializer
29+
abstract class Deserializer
2830
{
2931
/**
3032
* @var array<string, array<string, mixed>>
@@ -42,221 +44,202 @@ public function __construct(
4244
}
4345

4446
/**
47+
* @param T $data
4548
* @param array<string, mixed> $context
4649
*/
47-
public function deserialize(mixed $data, Type|UnionType $type, array $context): mixed
48-
{
49-
if ($type instanceof UnionType) {
50-
if (!isset($context['union_selector'][$typeString = (string) $type])) {
51-
throw new UnexpectedValueException(sprintf('Cannot guess type to use for "%s", you may specify a type in "$context[\'union_selector\'][\'%1$s\']".', (string) $type));
52-
}
53-
54-
/** @var Type $type */
55-
$type = (self::$cache['type'][$typeString] ??= TypeFactory::createFromString($context['union_selector'][$typeString]));
56-
}
57-
58-
$result = match (true) {
59-
$type->isScalar() => $this->deserializeScalar($data, $type, $context),
60-
$type->isCollection() => $this->deserializeCollection($data, $type, $context),
61-
$type->isEnum() => $this->deserializeEnum($data, $type, $context),
62-
$type->isObject() => $this->deserializeObject($data, $type, $context),
63-
64-
default => throw new UnsupportedTypeException($type),
65-
};
66-
67-
if (null === $result && !$type->isNullable()) {
68-
throw new UnexpectedValueException(sprintf('Unexpected "null" value for "%s" type.', (string) $type));
69-
}
70-
71-
return $result;
72-
}
50+
abstract protected function deserializeScalar(mixed $data, Type $type, array $context): mixed;
7351

7452
/**
53+
* @param T $data
7554
* @param array<string, mixed> $context
7655
*/
77-
protected function deserializeScalar(mixed $data, Type $type, array $context): int|string|bool|float|null
78-
{
79-
if (null === $data) {
80-
return null;
81-
}
82-
83-
try {
84-
return match ($type->name()) {
85-
'int' => (int) $data,
86-
'float' => (float) $data,
87-
'string' => (string) $data,
88-
'bool' => (bool) $data,
89-
default => throw new LogicException(sprintf('Unhandled "%s" scalar cast', $type->name())),
90-
};
91-
} catch (\Throwable) {
92-
throw new UnexpectedValueException(sprintf('Cannot cast "%s" to "%s"', get_debug_type($data), (string) $type));
93-
}
94-
}
56+
abstract protected function deserializeEnum(mixed $data, Type $type, array $context): mixed;
9557

9658
/**
59+
* @param T $data
9760
* @param array<string, mixed> $context
9861
*
99-
* @return \Iterator<mixed>|\Iterator<string, mixed>|list<mixed>|array<string, mixed>|null
62+
* @return \Iterator<mixed>|null
10063
*/
101-
protected function deserializeCollection(mixed $data, Type $type, array $context): \Iterator|array|null
102-
{
103-
if (null === $data) {
104-
return null;
105-
}
106-
107-
$data = $this->deserializeCollectionItems($data, $type->collectionValueType(), $context);
64+
abstract protected function deserializeList(mixed $data, Type $type, array $context): ?\Iterator;
10865

109-
return $type->isIterable() ? $data : iterator_to_array($data);
110-
}
66+
/**
67+
* @param T $data
68+
* @param array<string, mixed> $context
69+
*
70+
* @return \Iterator<string, mixed>|null
71+
*/
72+
abstract protected function deserializeDict(mixed $data, Type $type, array $context): ?\Iterator;
11173

11274
/**
75+
* @param T $data
11376
* @param array<string, mixed> $context
77+
*
78+
* @return \Iterator<string, mixed>|array<string, mixed>|null
11479
*/
115-
protected function deserializeEnum(mixed $data, Type $type, array $context): ?\BackedEnum
116-
{
117-
if (null === $data) {
118-
return null;
119-
}
80+
abstract protected function deserializeObjectProperties(mixed $data, Type $type, array $context): \Iterator|array|null;
12081

121-
try {
122-
return ($type->className())::from($data);
123-
} catch (\ValueError $e) {
124-
throw new UnexpectedValueException(sprintf('Unexpected "%s" value for "%s" backed enumeration.', $data, $type));
125-
}
126-
}
82+
/**
83+
* @param T $data
84+
* @param array<string, mixed> $context
85+
*
86+
* @return callable(): mixed
87+
*/
88+
abstract protected function propertyValueCallable(Type|UnionType $type, mixed $data, mixed $value, array $context): callable;
12789

12890
/**
91+
* @param T $data
12992
* @param array<string, mixed> $context
13093
*/
131-
protected function deserializeObject(mixed $data, Type $type, array $context): ?object
94+
final public function deserialize(mixed $data, Type|UnionType $type, array $context): mixed
13295
{
133-
if (null === $data) {
134-
return null;
135-
}
136-
137-
$hook = null;
96+
if ($type instanceof UnionType) {
97+
if (!isset($context['union_selector'][$typeString = (string) $type])) {
98+
throw new UnexpectedValueException(sprintf('Cannot guess type to use for "%s", you may specify a type in "$context[\'union_selector\'][\'%1$s\']".', (string) $type));
99+
}
138100

139-
if (isset($context['hooks']['deserialize'][$className = $type->className()])) {
140-
$hook = $context['hooks']['deserialize'][$className];
141-
} elseif (isset($context['hooks']['deserialize']['object'])) {
142-
$hook = $context['hooks']['deserialize']['object'];
101+
/** @var Type $type */
102+
$type = (self::$cache['type'][$typeString] ??= TypeFactory::createFromString($context['union_selector'][$typeString]));
143103
}
144104

145-
if (null !== $hook) {
146-
/** @var array{type?: string, context?: array<string, mixed>} $hookResult */
147-
$hookResult = $hook((string) $type, $context);
105+
if ($type->isScalar()) {
106+
$scalar = $this->deserializeScalar($data, $type, $context);
107+
108+
if (null === $scalar) {
109+
if (!$type->isNullable()) {
110+
throw new UnexpectedValueException(sprintf('Unexpected "null" value for "%s" type.', (string) $type));
111+
}
148112

149-
if (isset($hookResult['type'])) {
150-
/** @var Type $type */
151-
$type = (self::$cache['type'][$hookResult['type']] ??= TypeFactory::createFromString($hookResult['type']));
113+
return null;
152114
}
153115

154-
$context = $hookResult['context'] ?? $context;
116+
try {
117+
return match ($type->name()) {
118+
'int' => (int) $scalar,
119+
'float' => (float) $scalar,
120+
'string' => (string) $scalar,
121+
'bool' => (bool) $scalar,
122+
default => throw new LogicException(sprintf('Unhandled "%s" scalar cast', $type->name())),
123+
};
124+
} catch (\Throwable) {
125+
throw new UnexpectedValueException(sprintf('Cannot cast "%s" to "%s"', get_debug_type($scalar), (string) $type));
126+
}
155127
}
156128

157-
/** @var \ReflectionClass<object> $reflection */
158-
$reflection = (self::$cache['class_reflection'][$typeString = (string) $type] ??= new \ReflectionClass($type->className()));
129+
if ($type->isEnum()) {
130+
$enum = $this->deserializeEnum($data, $type, $context);
159131

160-
/** @var array<string, callable(): mixed> $propertiesValues */
161-
$propertiesValues = [];
132+
if (null === $enum) {
133+
if (!$type->isNullable()) {
134+
throw new UnexpectedValueException(sprintf('Unexpected "null" value for "%s" type.', (string) $type));
135+
}
162136

163-
foreach ($data as $k => $v) {
164-
$hook = null;
137+
return null;
138+
}
165139

166-
if (isset($context['hooks']['deserialize'][($className = $reflection->getName()).'['.$k.']'])) {
167-
$hook = $context['hooks']['deserialize'][$className.'['.$k.']'];
168-
} elseif (isset($context['hooks']['deserialize']['property'])) {
169-
$hook = $context['hooks']['deserialize']['property'];
140+
try {
141+
return ($type->className())::from($enum);
142+
} catch (\ValueError $e) {
143+
throw new UnexpectedValueException(sprintf('Unexpected "%s" value for "%s" backed enumeration.', $enum, $type));
170144
}
145+
}
171146

172-
$propertyName = $k;
147+
if ($type->isCollection()) {
148+
$collection = $type->isList() ? $this->deserializeList($data, $type, $context) : $this->deserializeDict($data, $type, $context);
173149

174-
if (null !== $hook) {
175-
$hookResult = $this->executePropertyHook($hook, $reflection, $k, $v, $data, $context);
150+
if (null === $collection) {
151+
if (!$type->isNullable()) {
152+
throw new UnexpectedValueException(sprintf('Unexpected "null" value for "%s" type.', (string) $type));
153+
}
176154

177-
$propertyName = $hookResult['name'] ?? $propertyName;
178-
$context = $hookResult['context'] ?? $context;
155+
return null;
179156
}
180157

181-
self::$cache['class_has_property'][$propertyIdentifier = $typeString.$propertyName] ??= $reflection->hasProperty($propertyName);
158+
return $type->isIterable() ? $collection : iterator_to_array($collection);
159+
}
160+
161+
if ($type->isObject()) {
162+
$objectProperties = $this->deserializeObjectProperties($data, $type, $context);
163+
164+
if (null === $objectProperties) {
165+
if (!$type->isNullable()) {
166+
throw new UnexpectedValueException(sprintf('Unexpected "null" value for "%s" type.', (string) $type));
167+
}
182168

183-
if (!self::$cache['class_has_property'][$propertyIdentifier]) {
184-
continue;
169+
return null;
185170
}
186171

187-
if (isset($hookResult['value_provider'])) {
188-
$propertiesValues[$propertyName] = $hookResult['value_provider'];
172+
if (null !== $hook = $context['hooks']['deserialize'][$type->className()] ?? $context['hooks']['deserialize']['object'] ?? null) {
173+
/** @var array{type?: string, context?: array<string, mixed>} $hookResult */
174+
$hookResult = $hook((string) $type, $context);
175+
176+
if (isset($hookResult['type'])) {
177+
/** @var Type $type */
178+
$type = (self::$cache['type'][$hookResult['type']] ??= TypeFactory::createFromString($hookResult['type']));
179+
}
189180

190-
continue;
181+
$context = $hookResult['context'] ?? $context;
191182
}
192183

193-
self::$cache['property_type'][$propertyIdentifier] ??= TypeFactory::createFromString($this->reflectionTypeExtractor->extractFromProperty($reflection->getProperty($propertyName)));
184+
/** @var \ReflectionClass<object> $reflection */
185+
$reflection = (self::$cache['class_reflection'][$typeString = (string) $type] ??= new \ReflectionClass($type->className()));
194186

195-
$propertiesValues[$propertyName] = $this->propertyValue(self::$cache['property_type'][$propertyIdentifier], $v, $data, $context);
196-
}
187+
/** @var array<string, callable(): mixed> $valueCallables */
188+
$valueCallables = [];
197189

198-
if (isset($context['instantiator'])) {
199-
return $context['instantiator']($reflection, $propertiesValues, $context);
200-
}
190+
foreach ($objectProperties as $name => $value) {
191+
if (null !== $hook = $context['hooks']['deserialize'][$reflection->getName().'['.$name.']'] ?? $context['hooks']['deserialize']['property'] ?? null) {
192+
$hookResult = $hook(
193+
$reflection,
194+
$name,
195+
fn (string $type, array $context) => $this->propertyValueCallable(self::$cache['type'][$type] ??= TypeFactory::createFromString($type), $data, $value, $context)(),
196+
$context,
197+
);
201198

202-
$object = new ($reflection->getName())();
199+
$name = $hookResult['name'] ?? $name;
200+
$context = $hookResult['context'] ?? $context;
201+
}
203202

204-
foreach ($propertiesValues as $property => $value) {
205-
try {
206-
$object->{$property} = $value();
207-
} catch (\TypeError|UnexpectedValueException $e) {
208-
$exception = new UnexpectedValueException($e->getMessage(), previous: $e);
203+
self::$cache['class_has_property'][$identifier = $typeString.$name] ??= $reflection->hasProperty($name);
204+
205+
if (!self::$cache['class_has_property'][$identifier]) {
206+
continue;
207+
}
208+
209+
if (isset($hookResult['value_provider'])) {
210+
$valueCallables[$name] = $hookResult['value_provider'];
209211

210-
if (!($context['collect_errors'] ?? false)) {
211-
throw $exception;
212+
continue;
212213
}
213214

214-
$context['collected_errors'][] = $exception;
215+
self::$cache['property_type'][$identifier] ??= TypeFactory::createFromString($this->reflectionTypeExtractor->extractFromProperty($reflection->getProperty($name)));
216+
217+
$valueCallables[$name] = $this->propertyValueCallable(self::$cache['property_type'][$identifier], $data, $value, $context);
215218
}
216-
}
217219

218-
return $object;
219-
}
220+
if (isset($context['instantiator'])) {
221+
return $context['instantiator']($reflection, $valueCallables, $context);
222+
}
220223

221-
/**
222-
* @param callable(\ReflectionClass<object>, string, callable(string, array<string, mixed>): mixed, array<string, mixed>): array{name?: string, value_provider?: callable(): mixed, context?: array<string, mixed>} $hook
223-
* @param \ReflectionClass<object> $reflection
224-
* @param array<string, mixed> $context
225-
*
226-
* @return array{name?: string, value_provider?: callable(): mixed, context?: array<string, mixed>}
227-
*/
228-
protected function executePropertyHook(callable $hook, \ReflectionClass $reflection, string $key, mixed $value, mixed $data, array $context): array
229-
{
230-
return $hook(
231-
$reflection,
232-
$key,
233-
function (string $type, array $context) use ($value): mixed {
234-
return $this->deserialize($value, self::$cache['type'][$type] ??= TypeFactory::createFromString($type), $context);
235-
},
236-
$context,
237-
);
238-
}
224+
$object = new ($reflection->getName())();
239225

240-
/**
241-
* @param array<string, mixed> $context
242-
*
243-
* @return callable(): mixed
244-
*/
245-
protected function propertyValue(Type|UnionType $type, mixed $value, mixed $data, array $context): callable
246-
{
247-
return fn () => $this->deserialize($value, $type, $context);
248-
}
226+
foreach ($valueCallables as $name => $callable) {
227+
try {
228+
$object->{$name} = $callable();
229+
} catch (\TypeError|UnexpectedValueException $e) {
230+
$exception = new UnexpectedValueException($e->getMessage(), previous: $e);
249231

250-
/**
251-
* @param array<string, mixed>|list<mixed> $collection
252-
* @param array<string, mixed> $context
253-
*
254-
* @return \Iterator<mixed>|\Iterator<string, mixed>
255-
*/
256-
private function deserializeCollectionItems(array $collection, Type|UnionType $type, array $context): \Iterator
257-
{
258-
foreach ($collection as $key => $value) {
259-
yield $key => $this->deserialize($value, $type, $context);
232+
if (!($context['collect_errors'] ?? false)) {
233+
throw $exception;
234+
}
235+
236+
$context['collected_errors'][] = $exception;
237+
}
238+
}
239+
240+
return $object;
260241
}
242+
243+
throw new UnsupportedTypeException($type);
261244
}
262245
}

src/Symfony/Component/SerDes/Internal/Deserialize/DeserializerFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,6 @@ private static function json(bool $lazy, bool $validate): Deserializer
6060
);
6161
}
6262

63-
return new Deserializer(new ReflectionTypeExtractor());
63+
return new EagerDeserializer(new ReflectionTypeExtractor());
6464
}
6565
}

0 commit comments

Comments
 (0)