Skip to content

Commit 14d5083

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 ------- d0284f9 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays
2 parents b113334 + d0284f9 commit 14d5083

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

src/Symfony/Component/Serializer/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 <fabien@symfony.com>
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy