Skip to content

Commit c373ca6

Browse files
committed
bug #45884 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays (phramz)
This PR was merged into the 5.4 branch. Discussion ---------- [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? |no | Deprecations? |no | Tickets | Fix #45883 | License | MIT | Doc PR | - Commits ------- d0284f9cc6 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays
2 parents f777331 + e3f7819 commit c373ca6

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

Normalizer/AbstractObjectNormalizer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@ public function denormalize($data, string $type, string $format = null, array $c
351351

352352
$this->validateCallbackContext($context);
353353

354+
if (null === $data && isset($context['value_type']) && $context['value_type'] instanceof Type && $context['value_type']->isNullable()) {
355+
return null;
356+
}
357+
354358
$allowedAttributes = $this->getAllowedAttributes($type, $context, true);
355359
$normalizedData = $this->prepareForDenormalization($data);
356360
$extraAttributes = [];
@@ -524,6 +528,8 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
524528
if (\count($collectionKeyType = $type->getCollectionKeyTypes()) > 0) {
525529
[$context['key_type']] = $collectionKeyType;
526530
}
531+
532+
$context['value_type'] = $collectionValueType;
527533
} elseif ($type->isCollection() && \count($collectionValueType = $type->getCollectionValueTypes()) > 0 && Type::BUILTIN_TYPE_ARRAY === $collectionValueType[0]->getBuiltinType()) {
528534
// get inner type for any nested array
529535
[$innerType] = $collectionValueType;
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Normalizer;
13+
14+
use Doctrine\Common\Annotations\AnnotationReader;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
17+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
18+
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
19+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
20+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
21+
use Symfony\Component\Serializer\Mapping\ClassMetadata;
22+
use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
23+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
24+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
25+
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
26+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
27+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
28+
use Symfony\Component\Serializer\Serializer;
29+
30+
class MapDenormalizationTest extends TestCase
31+
{
32+
public function testMapOfStringToNullableObject()
33+
{
34+
$normalizedData = $this->getSerializer()->denormalize([
35+
'map' => [
36+
'assertDummyMapValue' => [
37+
'value' => 'foo',
38+
],
39+
'assertNull' => null,
40+
],
41+
], DummyMapOfStringToNullableObject::class);
42+
43+
$this->assertInstanceOf(DummyMapOfStringToNullableObject::class, $normalizedData);
44+
45+
// check nullable map value
46+
$this->assertIsArray($normalizedData->map);
47+
48+
$this->assertArrayHasKey('assertDummyMapValue', $normalizedData->map);
49+
$this->assertInstanceOf(DummyValue::class, $normalizedData->map['assertDummyMapValue']);
50+
51+
$this->assertArrayHasKey('assertNull', $normalizedData->map);
52+
53+
$this->assertNull($normalizedData->map['assertNull']);
54+
}
55+
56+
public function testMapOfStringToAbstractNullableObject()
57+
{
58+
$normalizedData = $this->getSerializer()->denormalize(
59+
[
60+
'map' => [
61+
'assertNull' => null,
62+
],
63+
], DummyMapOfStringToNullableAbstractObject::class);
64+
65+
$this->assertInstanceOf(DummyMapOfStringToNullableAbstractObject::class, $normalizedData);
66+
67+
$this->assertIsArray($normalizedData->map);
68+
$this->assertArrayHasKey('assertNull', $normalizedData->map);
69+
$this->assertNull($normalizedData->map['assertNull']);
70+
}
71+
72+
public function testMapOfStringToObject()
73+
{
74+
$normalizedData = $this->getSerializer()->denormalize(
75+
[
76+
'map' => [
77+
'assertDummyMapValue' => [
78+
'value' => 'foo',
79+
],
80+
'assertEmptyDummyMapValue' => null,
81+
],
82+
], DummyMapOfStringToObject::class);
83+
84+
$this->assertInstanceOf(DummyMapOfStringToObject::class, $normalizedData);
85+
86+
// check nullable map value
87+
$this->assertIsArray($normalizedData->map);
88+
89+
$this->assertArrayHasKey('assertDummyMapValue', $normalizedData->map);
90+
$this->assertInstanceOf(DummyValue::class, $normalizedData->map['assertDummyMapValue']);
91+
$this->assertEquals('foo', $normalizedData->map['assertDummyMapValue']->value);
92+
93+
$this->assertArrayHasKey('assertEmptyDummyMapValue', $normalizedData->map);
94+
$this->assertInstanceOf(DummyValue::class, $normalizedData->map['assertEmptyDummyMapValue']); // correct since to attribute is not nullable
95+
$this->assertNull($normalizedData->map['assertEmptyDummyMapValue']->value);
96+
}
97+
98+
public function testMapOfStringToAbstractObject()
99+
{
100+
$normalizedData = $this->getSerializer()->denormalize(
101+
[
102+
'map' => [
103+
'assertDummyMapValue' => [
104+
'type' => 'dummy',
105+
'value' => 'foo',
106+
],
107+
],
108+
], DummyMapOfStringToNotNullableAbstractObject::class);
109+
110+
$this->assertInstanceOf(DummyMapOfStringToNotNullableAbstractObject::class, $normalizedData);
111+
112+
// check nullable map value
113+
$this->assertIsArray($normalizedData->map);
114+
115+
$this->assertArrayHasKey('assertDummyMapValue', $normalizedData->map);
116+
$this->assertInstanceOf(DummyValue::class, $normalizedData->map['assertDummyMapValue']);
117+
$this->assertEquals('foo', $normalizedData->map['assertDummyMapValue']->value);
118+
}
119+
120+
public function testMapOfStringToAbstractObjectMissingTypeAttribute()
121+
{
122+
$this->expectException(NotNormalizableValueException::class);
123+
$this->expectExceptionMessage('Type property "type" not found for the abstract object "Symfony\Component\Serializer\Tests\Normalizer\AbstractDummyValue".');
124+
125+
$this->getSerializer()->denormalize(
126+
[
127+
'map' => [
128+
'assertEmptyDummyMapValue' => null,
129+
],
130+
], DummyMapOfStringToNotNullableAbstractObject::class);
131+
}
132+
133+
public function testNullableObject()
134+
{
135+
$normalizedData = $this->getSerializer()->denormalize(
136+
[
137+
'object' => [
138+
'value' => 'foo',
139+
],
140+
'nullObject' => null,
141+
], DummyNullableObjectValue::class);
142+
143+
$this->assertInstanceOf(DummyNullableObjectValue::class, $normalizedData);
144+
145+
$this->assertInstanceOf(DummyValue::class, $normalizedData->object);
146+
$this->assertEquals('foo', $normalizedData->object->value);
147+
148+
$this->assertNull($normalizedData->nullObject);
149+
}
150+
151+
public function testNotNullableObject()
152+
{
153+
$normalizedData = $this->getSerializer()->denormalize(
154+
[
155+
'object' => [
156+
'value' => 'foo',
157+
],
158+
'nullObject' => null,
159+
], DummyNotNullableObjectValue::class);
160+
161+
$this->assertInstanceOf(DummyNotNullableObjectValue::class, $normalizedData);
162+
163+
$this->assertInstanceOf(DummyValue::class, $normalizedData->object);
164+
$this->assertEquals('foo', $normalizedData->object->value);
165+
166+
$this->assertInstanceOf(DummyValue::class, $normalizedData->nullObject);
167+
$this->assertNull($normalizedData->nullObject->value);
168+
}
169+
170+
public function testNullableAbstractObject()
171+
{
172+
$normalizedData = $this->getSerializer()->denormalize(
173+
[
174+
'object' => [
175+
'type' => 'another-dummy',
176+
'value' => 'foo',
177+
],
178+
'nullObject' => null,
179+
], DummyNullableAbstractObjectValue::class);
180+
181+
$this->assertInstanceOf(DummyNullableAbstractObjectValue::class, $normalizedData);
182+
183+
$this->assertInstanceOf(AnotherDummyValue::class, $normalizedData->object);
184+
$this->assertEquals('foo', $normalizedData->object->value);
185+
186+
$this->assertNull($normalizedData->nullObject);
187+
}
188+
189+
private function getSerializer()
190+
{
191+
$loaderMock = new class() implements ClassMetadataFactoryInterface {
192+
public function getMetadataFor($value): ClassMetadataInterface
193+
{
194+
if (AbstractDummyValue::class === $value) {
195+
return new ClassMetadata(
196+
AbstractDummyValue::class,
197+
new ClassDiscriminatorMapping('type', [
198+
'dummy' => DummyValue::class,
199+
'another-dummy' => AnotherDummyValue::class,
200+
])
201+
);
202+
}
203+
204+
throw new InvalidArgumentException();
205+
}
206+
207+
public function hasMetadataFor($value): bool
208+
{
209+
return AbstractDummyValue::class === $value;
210+
}
211+
};
212+
213+
$factory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
214+
$normalizer = new ObjectNormalizer($factory, null, null, new PhpDocExtractor(), new ClassDiscriminatorFromClassMetadata($loaderMock));
215+
$serializer = new Serializer([$normalizer, new ArrayDenormalizer()]);
216+
$normalizer->setSerializer($serializer);
217+
218+
return $serializer;
219+
}
220+
}
221+
222+
abstract class AbstractDummyValue
223+
{
224+
public $value;
225+
}
226+
227+
class DummyValue extends AbstractDummyValue
228+
{
229+
}
230+
231+
class AnotherDummyValue extends AbstractDummyValue
232+
{
233+
}
234+
235+
class DummyNotNullableObjectValue
236+
{
237+
/**
238+
* @var DummyValue
239+
*/
240+
public $object;
241+
242+
/**
243+
* @var DummyValue
244+
*/
245+
public $nullObject;
246+
}
247+
248+
class DummyNullableObjectValue
249+
{
250+
/**
251+
* @var DummyValue|null
252+
*/
253+
public $object;
254+
255+
/**
256+
* @var DummyValue|null
257+
*/
258+
public $nullObject;
259+
}
260+
261+
class DummyNullableAbstractObjectValue
262+
{
263+
/**
264+
* @var AbstractDummyValue|null
265+
*/
266+
public $object;
267+
268+
/**
269+
* @var AbstractDummyValue|null
270+
*/
271+
public $nullObject;
272+
}
273+
274+
class DummyMapOfStringToNullableObject
275+
{
276+
/**
277+
* @var array<string,DummyValue|null>
278+
*/
279+
public $map;
280+
}
281+
282+
class DummyMapOfStringToObject
283+
{
284+
/**
285+
* @var array<string,DummyValue>
286+
*/
287+
public $map;
288+
}
289+
290+
class DummyMapOfStringToNullableAbstractObject
291+
{
292+
/**
293+
* @var array<string,AbstractDummyValue|null>
294+
*/
295+
public $map;
296+
}
297+
298+
class DummyMapOfStringToNotNullableAbstractObject
299+
{
300+
/**
301+
* @var array<string,AbstractDummyValue>
302+
*/
303+
public $map;
304+
}

0 commit comments

Comments
 (0)