Skip to content

Commit f916587

Browse files
committed
handle mixed, object and raw array
1 parent d88381d commit f916587

File tree

15 files changed

+275
-69
lines changed

15 files changed

+275
-69
lines changed

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\SerDes\Exception\LogicException;
1515
use Symfony\Component\SerDes\Exception\UnexpectedValueException;
16-
use Symfony\Component\SerDes\Exception\UnsupportedTypeException;
1716
use Symfony\Component\SerDes\Internal\Type;
1817
use Symfony\Component\SerDes\Internal\TypeFactory;
1918
use Symfony\Component\SerDes\Internal\UnionType;
@@ -79,6 +78,14 @@ abstract protected function deserializeDict(mixed $data, Type $type, array $cont
7978
*/
8079
abstract protected function deserializeObjectProperties(mixed $data, Type $type, array $context): \Iterator|array|null;
8180

81+
/**
82+
* @param T $data
83+
* @param array<string, mixed> $context
84+
*
85+
* @return T
86+
*/
87+
abstract protected function deserializeMixed(mixed $data, Type $type, array $context): mixed;
88+
8289
/**
8390
* @param T $data
8491
* @param array<string, mixed> $context
@@ -145,7 +152,13 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
145152
}
146153

147154
if ($type->isCollection()) {
148-
$collection = $type->isList() ? $this->deserializeList($data, $type, $context) : $this->deserializeDict($data, $type, $context);
155+
if ($type->isList()) {
156+
$collection = $this->deserializeList($data, $type, $context);
157+
} elseif ($type->isDict()) {
158+
$collection = $this->deserializeDict($data, $type, $context);
159+
} else {
160+
$collection = $this->deserializeMixed($data, $type, $context);
161+
}
149162

150163
if (null === $collection) {
151164
if (!$type->isNullable()) {
@@ -159,6 +172,17 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
159172
}
160173

161174
if ($type->isObject()) {
175+
try {
176+
$className = $type->className();
177+
} catch (LogicException) {
178+
$object = new \stdClass();
179+
foreach ($this->deserializeMixed($data, $type, $context) as $property => $value) {
180+
$object->{$property} = $value;
181+
}
182+
183+
return $object;
184+
}
185+
162186
$objectProperties = $this->deserializeObjectProperties($data, $type, $context);
163187

164188
if (null === $objectProperties) {
@@ -169,7 +193,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
169193
return null;
170194
}
171195

172-
if (null !== $hook = $context['hooks']['deserialize'][$type->className()] ?? $context['hooks']['deserialize']['object'] ?? null) {
196+
if (null !== $hook = $context['hooks']['deserialize'][$className] ?? $context['hooks']['deserialize']['object'] ?? null) {
173197
/** @var array{type?: string, context?: array<string, mixed>} $hookResult */
174198
$hookResult = $hook((string) $type, $context);
175199

@@ -182,13 +206,13 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
182206
}
183207

184208
/** @var \ReflectionClass<object> $reflection */
185-
$reflection = (self::$cache['class_reflection'][$typeString = (string) $type] ??= new \ReflectionClass($type->className()));
209+
$reflection = (self::$cache['class_reflection'][$typeString = (string) $type] ??= new \ReflectionClass($className));
186210

187211
/** @var array<string, callable(): mixed> $valueCallables */
188212
$valueCallables = [];
189213

190214
foreach ($objectProperties as $name => $value) {
191-
if (null !== $hook = $context['hooks']['deserialize'][$reflection->getName().'['.$name.']'] ?? $context['hooks']['deserialize']['property'] ?? null) {
215+
if (null !== $hook = $context['hooks']['deserialize'][$className.'['.$name.']'] ?? $context['hooks']['deserialize']['property'] ?? null) {
192216
$hookResult = $hook(
193217
$reflection,
194218
$name,
@@ -221,7 +245,7 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
221245
return $context['instantiator']($reflection, $valueCallables, $context);
222246
}
223247

224-
$object = new ($reflection->getName())();
248+
$object = new $className();
225249

226250
foreach ($valueCallables as $name => $callable) {
227251
try {
@@ -240,6 +264,6 @@ final public function deserialize(mixed $data, Type|UnionType $type, array $cont
240264
return $object;
241265
}
242266

243-
throw new UnsupportedTypeException($type);
267+
return $this->deserializeMixed($data, $type, $context);
244268
}
245269
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ private function __construct()
3232

3333
/**
3434
* @param array<string, mixed> $context
35+
*
36+
* @return Deserializer<mixed>
3537
*/
3638
public static function create(string $format, array $context): Deserializer
3739
{
@@ -44,6 +46,9 @@ public static function create(string $format, array $context): Deserializer
4446
};
4547
}
4648

49+
/**
50+
* @return Deserializer<mixed>
51+
*/
4752
private static function json(bool $lazy, bool $validate): Deserializer
4853
{
4954
if ($lazy) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ protected function deserializeDict(mixed $data, Type $type, array $context): ?\I
6060
return $this->deserializeCollectionItems($data, $type->collectionValueType(), $context);
6161
}
6262

63+
/**
64+
* @return array<string, mixed>|null
65+
*/
6366
protected function deserializeObjectProperties(mixed $data, Type $type, array $context): ?array
6467
{
6568
if (null === $data) {
@@ -73,6 +76,11 @@ protected function deserializeObjectProperties(mixed $data, Type $type, array $c
7376
return $data;
7477
}
7578

79+
protected function deserializeMixed(mixed $data, Type $type, array $context): mixed
80+
{
81+
return $data;
82+
}
83+
7684
protected function propertyValueCallable(Type|UnionType $type, mixed $data, mixed $value, array $context): callable
7785
{
7886
return fn () => $this->deserialize($value, $type, $context);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ protected function deserializeObjectProperties(mixed $data, Type $type, array $c
6666
return $this->dictSplitter->split($data, $type, $context);
6767
}
6868

69+
protected function deserializeMixed(mixed $data, Type $type, array $context): mixed
70+
{
71+
return $this->decoder->decode($data, $context['boundary'][0], $context['boundary'][1], $context);
72+
}
73+
6974
protected function propertyValueCallable(Type|UnionType $type, mixed $data, mixed $value, array $context): callable
7075
{
7176
return fn () => $this->deserialize($data, $type, ['boundary' => $value] + $context);

src/Symfony/Component/SerDes/Internal/Serialize/TemplateGenerator/JsonTemplateGenerator.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ protected function objectNodes(Type $type, array $propertiesInfo, array $context
130130
return $nodes;
131131
}
132132

133+
protected function mixedNodes(NodeInterface $accessor, array $context): array
134+
{
135+
return [
136+
new ExpressionNode(new FunctionNode('\fwrite', [new VariableNode('resource'), $this->encodeValueNode($accessor)])),
137+
];
138+
}
139+
133140
private function encodeValueNode(NodeInterface $node): NodeInterface
134141
{
135142
return new FunctionNode('\json_encode', [

src/Symfony/Component/SerDes/Internal/Serialize/TemplateGenerator/TemplateGenerator.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Component\SerDes\Exception\CircularReferenceException;
1515
use Symfony\Component\SerDes\Exception\LogicException;
16-
use Symfony\Component\SerDes\Exception\UnsupportedTypeException;
1716
use Symfony\Component\SerDes\Internal\Serialize\Compiler;
1817
use Symfony\Component\SerDes\Internal\Serialize\Node\AssignNode;
1918
use Symfony\Component\SerDes\Internal\Serialize\Node\BinaryNode;
@@ -91,6 +90,13 @@ abstract protected function dictNodes(Type $type, NodeInterface $accessor, array
9190
*/
9291
abstract protected function objectNodes(Type $type, array $propertiesInfo, array $context): array;
9392

93+
/**
94+
* @param array<string, mixed> $context
95+
*
96+
* @return list<NodeInterface>
97+
*/
98+
abstract protected function mixedNodes(NodeInterface $accessor, array $context): array;
99+
94100
/**
95101
* @param array<string, mixed> $context
96102
*
@@ -174,7 +180,13 @@ private function nodes(Type|UnionType $type, NodeInterface $accessor, array $con
174180
}
175181

176182
if ($type->isObject()) {
177-
if (null !== $hook = $context['hooks']['serialize'][$className = $type->className()] ?? $context['hooks']['serialize']['object'] ?? null) {
183+
try {
184+
$className = $type->className();
185+
} catch (LogicException) {
186+
return $this->mixedNodes($accessor, $context);
187+
}
188+
189+
if (null !== $hook = $context['hooks']['serialize'][$className] ?? $context['hooks']['serialize']['object'] ?? null) {
178190
$hookResult = $hook((string) $type, (new Compiler())->compile($accessor)->source(), $context);
179191

180192
/** @var Type $type */
@@ -183,7 +195,7 @@ private function nodes(Type|UnionType $type, NodeInterface $accessor, array $con
183195
$context = $hookResult['context'] ?? $context;
184196
}
185197

186-
if (isset($context['generated_classes'][$className = $type->className()])) {
198+
if (isset($context['generated_classes'][$className])) {
187199
throw new CircularReferenceException($className);
188200
}
189201

@@ -199,7 +211,7 @@ private function nodes(Type|UnionType $type, NodeInterface $accessor, array $con
199211
];
200212
}
201213

202-
throw new UnsupportedTypeException((string) $type);
214+
return $this->mixedNodes($accessor, $context);
203215
}
204216

205217
/**
@@ -261,6 +273,14 @@ private function typeValidatorNode(Type $type, NodeInterface $accessor): NodeInt
261273
return new BinaryNode('instanceof', $accessor, new ScalarNode($type->className()));
262274
}
263275

276+
if ('array' === $type->name()) {
277+
return new FunctionNode('\is_array', [$accessor]);
278+
}
279+
280+
if ('mixed' === $type->name()) {
281+
return new ScalarNode(true);
282+
}
283+
264284
throw new LogicException(sprintf('Cannot find validator for "%s".', (string) $type));
265285
}
266286
}

src/Symfony/Component/SerDes/Internal/Type.php

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,10 @@ public function __construct(
3434
private readonly bool $isGeneric = false,
3535
private readonly array $genericParameterTypes = [],
3636
) {
37-
if ($this->isObject() && null === $this->className) {
38-
throw new InvalidArgumentException('Missing className of "object" type.');
39-
}
40-
4137
if ($this->isGeneric && !$this->genericParameterTypes) {
4238
throw new InvalidArgumentException(sprintf('Missing generic parameter types of "%s" type.', $this->name));
4339
}
4440

45-
if ($this->isCollection() && 2 !== \count($this->genericParameterTypes)) {
46-
throw new InvalidArgumentException(sprintf('Invalid generic parameter types of "%s" type.', $this->name));
47-
}
48-
4941
$this->stringValue = $this->computeStringValue();
5042
}
5143

@@ -68,10 +60,11 @@ public function className(): string
6860
throw new LogicException(sprintf('Cannot get class on "%s" type as it\'s not an object nor an enum.', $this->name));
6961
}
7062

71-
/** @var class-string $className */
72-
$className = $this->className;
63+
if (null === $this->className) {
64+
throw new LogicException(sprintf('No class has been defined for "%s".', $this->name));
65+
}
7366

74-
return $className;
67+
return $this->className;
7568
}
7669

7770
/**
@@ -133,7 +126,16 @@ public function isList(): bool
133126

134127
public function isDict(): bool
135128
{
136-
return $this->isCollection() && !$this->isList();
129+
if (!$this->isCollection()) {
130+
return false;
131+
}
132+
133+
$collectionKeyType = $this->collectionKeyType();
134+
if (!$collectionKeyType instanceof self) {
135+
return false;
136+
}
137+
138+
return 'string' === $collectionKeyType->name();
137139
}
138140

139141
public function collectionKeyType(): self|UnionType
@@ -142,7 +144,7 @@ public function collectionKeyType(): self|UnionType
142144
throw new LogicException(sprintf('Cannot get collection key type on "%s" type as it\'s not a collection.', $this->name));
143145
}
144146

145-
return $this->genericParameterTypes[0];
147+
return $this->genericParameterTypes[0] ?? new self('mixed');
146148
}
147149

148150
public function collectionValueType(): self|UnionType
@@ -151,7 +153,7 @@ public function collectionValueType(): self|UnionType
151153
throw new LogicException(sprintf('Cannot get collection value type on "%s" type as it\'s not a collection.', $this->name));
152154
}
153155

154-
return $this->genericParameterTypes[1];
156+
return $this->genericParameterTypes[1] ?? new self('mixed');
155157
}
156158

157159
private function computeStringValue(): string
@@ -162,9 +164,10 @@ private function computeStringValue(): string
162164

163165
$nullablePrefix = $this->isNullable() ? '?' : '';
164166

165-
$name = $this->name();
166-
if ($this->isObject() || $this->isEnum()) {
167+
try {
167168
$name = $this->className();
169+
} catch (LogicException) {
170+
$name = $this->name();
168171
}
169172

170173
if ($this->isGeneric()) {

src/Symfony/Component/SerDes/Tests/Internal/Deserialize/DeserializeTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,36 @@ public function testDeserializeUnionType(callable $deserialize)
4949
$this->assertSame([1, 2, 3], $deserialize('[1, "2", "3"]', 'array<int, int|string>'));
5050
}
5151

52+
/**
53+
* @dataProvider deserializeDataProvider
54+
*
55+
* @param callable(string, string, array<string, mixed>): mixed
56+
*/
57+
public function testDeserializeRawArray(callable $deserialize)
58+
{
59+
$this->assertSame([['foo' => 1, 'bar' => 2], ['baz' => 3]], $deserialize('[{"foo": 1, "bar": 2}, {"baz": 3}]', 'array'));
60+
}
61+
62+
/**
63+
* @dataProvider deserializeDataProvider
64+
*
65+
* @param callable(string, string, array<string, mixed>): mixed
66+
*/
67+
public function testDeserializeArray(callable $deserialize)
68+
{
69+
$this->assertSame([['foo' => 1, 'bar' => 2], ['baz' => 3]], $deserialize('[{"foo": 1, "bar": 2}, {"baz": 3}]', 'array<int, array<string, int>>'));
70+
}
71+
72+
/**
73+
* @dataProvider deserializeDataProvider
74+
*
75+
* @param callable(string, string, array<string, mixed>): mixed
76+
*/
77+
public function testDeserializeRawIterable(callable $deserialize)
78+
{
79+
$this->assertSame([['foo' => 1, 'bar' => 2], ['baz' => 3]], $deserialize('[{"foo": 1, "bar": 2}, {"baz": 3}]', 'iterable'));
80+
}
81+
5282
/**
5383
* @dataProvider deserializeDataProvider
5484
*
@@ -69,6 +99,22 @@ public function testDeserializeIterable(callable $deserialize)
6999
$this->assertSame([['foo' => 1, 'bar' => 2], ['baz' => 3]], $result);
70100
}
71101

102+
/**
103+
* @dataProvider deserializeDataProvider
104+
*
105+
* @param callable(string, string, array<string, mixed>): mixed
106+
*/
107+
public function testDeserializeRawObject(callable $deserialize)
108+
{
109+
$value = $deserialize('{"id": 123, "name": "thename"}', 'object');
110+
111+
$expectedObject = new \stdClass();
112+
$expectedObject->id = 123;
113+
$expectedObject->name = 'thename';
114+
115+
$this->assertEquals($expectedObject, $value);
116+
}
117+
72118
/**
73119
* @dataProvider deserializeDataProvider
74120
*
@@ -115,6 +161,16 @@ public function testDeserializeObject(callable $deserialize)
115161
$this->assertEquals($expectedObject, $value);
116162
}
117163

164+
/**
165+
* @dataProvider deserializeDataProvider
166+
*
167+
* @param callable(string, string, array<string, mixed>): mixed
168+
*/
169+
public function testDeserializeMixed(callable $deserialize)
170+
{
171+
$this->assertSame(['foo' => true], $deserialize('{"foo": true}', 'mixed'));
172+
}
173+
118174
/**
119175
* @dataProvider deserializeDataProvider
120176
*

0 commit comments

Comments
 (0)