Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fa2065a

Browse files
committed
Fix denormalizing nested arrays as object values
1 parent caa2579 commit fa2065a

File tree

4 files changed

+213
-65
lines changed

4 files changed

+213
-65
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -322,23 +322,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
322322
return;
323323
}
324324

325-
if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
326-
$builtinType = Type::BUILTIN_TYPE_OBJECT;
327-
$class = $collectionValueType->getClassName().'[]';
328-
329-
// Fix a collection that contains the only one element
330-
// This is special to xml format only
331-
if ('xml' === $format && !\is_int(key($data))) {
332-
$data = [$data];
333-
}
334-
335-
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
336-
$context['key_type'] = $collectionKeyType;
337-
}
338-
} else {
339-
$builtinType = $type->getBuiltinType();
340-
$class = $type->getClassName();
341-
}
325+
[$builtinType, $class] = $this->flatternDenormalizationType($type, $context);
342326

343327
$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
344328

@@ -375,6 +359,29 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
375359
throw new NotNormalizableValueException(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), \gettype($data)));
376360
}
377361

362+
private function flatternDenormalizationType(Type $type, array &$context): array
363+
{
364+
if ($type->isCollection() && ($collectionValueType = $type->getCollectionValueType()) && (Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType() || Type::BUILTIN_TYPE_ARRAY === $collectionValueType->getBuiltinType())) {
365+
if ($collectionValueType->isCollection()) {
366+
[$builtinType, $class] = $this->flatternDenormalizationType($collectionValueType, $context);
367+
} else {
368+
[$builtinType, $class] = [$collectionValueType->getBuiltinType(), $collectionValueType->getClassName()];
369+
}
370+
371+
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
372+
$class = $class.'[]';
373+
374+
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
375+
$context['key_types'][] = $collectionKeyType;
376+
}
377+
}
378+
} else {
379+
return [$type->getBuiltinType(), $type->getClassName()];
380+
}
381+
382+
return [$builtinType, $class];
383+
}
384+
378385
/**
379386
* @internal
380387
*/

src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\Serializer\Encoder\XmlEncoder;
1415
use Symfony\Component\Serializer\Exception\BadMethodCallException;
1516
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1617
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -51,7 +52,18 @@ public function denormalize($data, $class, $format = null, array $context = [])
5152
$serializer = $this->serializer;
5253
$class = substr($class, 0, -2);
5354

54-
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
55+
if (isset($context['key_types']) && \count($context['key_types'])) {
56+
$builtinType = array_pop($context['key_types'])->getBuiltinType();
57+
} else {
58+
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
59+
}
60+
61+
// Fix a collection that contains the only one element
62+
// This is special to xml format only
63+
if (XmlEncoder::FORMAT === $format && (null === $builtinType ? !\is_int(key($data)) : !('\is_'.$builtinType)(key($data)))) {
64+
$data = [$data];
65+
}
66+
5567
foreach ($data as $key => $value) {
5668
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
5769
throw new NotNormalizableValueException(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, \gettype($key)));

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
1717
use Symfony\Component\PropertyInfo\Type;
18-
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
1918
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
2019
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
2120
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
21+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
2222
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2323
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
24-
use Symfony\Component\Serializer\SerializerAwareInterface;
2524
use Symfony\Component\Serializer\SerializerInterface;
2625

2726
class AbstractObjectNormalizerTest extends TestCase
@@ -78,6 +77,31 @@ public function testDenormalizeWithExtraAttributesAndNoGroupsWithMetadataFactory
7877
);
7978
}
8079

80+
public function testDenormalizeDeepCollection()
81+
{
82+
$denormalizer = $this->getDenormalizerForDummyDeepCollection();
83+
84+
$dummyCollection = $denormalizer->denormalize(
85+
[
86+
'children' => [
87+
[
88+
[
89+
'bar' => 'first',
90+
],
91+
],
92+
],
93+
],
94+
DummyDeepCollection::class
95+
);
96+
97+
$this->assertInstanceOf(DummyDeepCollection::class, $dummyCollection);
98+
$this->assertInternalType('array', $dummyCollection->children);
99+
$this->assertCount(1, $dummyCollection->children);
100+
$this->assertInternalType('array', $dummyCollection->children[0]);
101+
$this->assertCount(1, $dummyCollection->children[0]);
102+
$this->assertInstanceOf(DummyChild::class, $dummyCollection->children[0][0]);
103+
}
104+
81105
public function testDenormalizeCollectionDecodedFromXmlWithOneChild()
82106
{
83107
$denormalizer = $this->getDenormalizerForDummyCollection();
@@ -139,7 +163,41 @@ private function getDenormalizerForDummyCollection()
139163
));
140164

141165
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
142-
$arrayDenormalizer = new ArrayDenormalizerDummy();
166+
$arrayDenormalizer = new ArrayDenormalizer();
167+
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
168+
$arrayDenormalizer->setSerializer($serializer);
169+
$denormalizer->setSerializer($serializer);
170+
171+
return $denormalizer;
172+
}
173+
174+
private function getDenormalizerForDummyDeepCollection()
175+
{
176+
$extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock();
177+
$extractor->method('getTypes')
178+
->will($this->onConsecutiveCalls(
179+
[
180+
new Type(
181+
'array',
182+
false,
183+
null,
184+
true,
185+
new Type('int'),
186+
new Type(
187+
'array',
188+
false,
189+
null,
190+
true,
191+
new Type('int'),
192+
new Type('object', false, DummyChild::class)
193+
)
194+
),
195+
],
196+
null
197+
));
198+
199+
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
200+
$arrayDenormalizer = new ArrayDenormalizer();
143201
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
144202
$arrayDenormalizer->setSerializer($serializer);
145203
$denormalizer->setSerializer($serializer);
@@ -233,6 +291,12 @@ class DummyCollection
233291
public $children;
234292
}
235293

294+
class DummyDeepCollection
295+
{
296+
/** @var DummyChild[][] */
297+
public $children;
298+
}
299+
236300
class DummyChild
237301
{
238302
public $bar;
@@ -306,45 +370,3 @@ public function deserialize($data, $type, $format, array $context = [])
306370
{
307371
}
308372
}
309-
310-
class ArrayDenormalizerDummy implements DenormalizerInterface, SerializerAwareInterface
311-
{
312-
/**
313-
* @var SerializerInterface|DenormalizerInterface
314-
*/
315-
private $serializer;
316-
317-
/**
318-
* {@inheritdoc}
319-
*
320-
* @throws NotNormalizableValueException
321-
*/
322-
public function denormalize($data, $class, $format = null, array $context = [])
323-
{
324-
$serializer = $this->serializer;
325-
$class = substr($class, 0, -2);
326-
327-
foreach ($data as $key => $value) {
328-
$data[$key] = $serializer->denormalize($value, $class, $format, $context);
329-
}
330-
331-
return $data;
332-
}
333-
334-
/**
335-
* {@inheritdoc}
336-
*/
337-
public function supportsDenormalization($data, $type, $format = null, array $context = [])
338-
{
339-
return '[]' === substr($type, -2)
340-
&& $this->serializer->supportsDenormalization($data, substr($type, 0, -2), $format, $context);
341-
}
342-
343-
/**
344-
* {@inheritdoc}
345-
*/
346-
public function setSerializer(SerializerInterface $serializer)
347-
{
348-
$this->serializer = $serializer;
349-
}
350-
}

src/Symfony/Component/Serializer/Tests/Normalizer/ArrayDenormalizerTest.php

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
namespace Symfony\Component\Serializer\Tests\Normalizer;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\PropertyInfo\Type;
1516
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
17+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
18+
use Symfony\Component\Serializer\Serializer;
1619
use Symfony\Component\Serializer\SerializerInterface;
1720

1821
class ArrayDenormalizerTest extends TestCase
@@ -38,12 +41,12 @@ public function testDenormalize()
3841
{
3942
$this->serializer->expects($this->at(0))
4043
->method('denormalize')
41-
->with(['foo' => 'one', 'bar' => 'two'])
44+
->with(['foo' => 'one', 'bar' => 'two'], __NAMESPACE__.'\ArrayDummy')
4245
->will($this->returnValue(new ArrayDummy('one', 'two')));
4346

4447
$this->serializer->expects($this->at(1))
4548
->method('denormalize')
46-
->with(['foo' => 'three', 'bar' => 'four'])
49+
->with(['foo' => 'three', 'bar' => 'four'], __NAMESPACE__.'\ArrayDummy')
4750
->will($this->returnValue(new ArrayDummy('three', 'four')));
4851

4952
$result = $this->denormalizer->denormalize(
@@ -63,6 +66,110 @@ public function testDenormalize()
6366
);
6467
}
6568

69+
public function testDenormalizeNested()
70+
{
71+
$m = $this->getMockBuilder(DenormalizerInterface::class)
72+
->getMock();
73+
74+
$denormalizer = new ArrayDenormalizer();
75+
$serializer = new Serializer([$denormalizer, $m]);
76+
$denormalizer->setSerializer($serializer);
77+
78+
$m->method('denormalize')
79+
->with(['foo' => 'one', 'bar' => 'two'], __NAMESPACE__.'\ArrayDummy')
80+
->willReturn(new ArrayDummy('one', 'two'));
81+
82+
$m->method('supportsDenormalization')
83+
->with($this->anything(), __NAMESPACE__.'\ArrayDummy')
84+
->willReturn(true);
85+
86+
$result = $denormalizer->denormalize(
87+
[
88+
[
89+
['foo' => 'one', 'bar' => 'two'],
90+
],
91+
],
92+
__NAMESPACE__.'\ArrayDummy[][]',
93+
null,
94+
['key_types' => [new Type('int'), new Type('int')]]
95+
);
96+
97+
$this->assertEquals(
98+
[[
99+
new ArrayDummy('one', 'two'),
100+
]],
101+
$result
102+
);
103+
}
104+
105+
/**
106+
* @expectedException \Symfony\Component\Serializer\Exception\NotNormalizableValueException
107+
*/
108+
public function testDenormalizeCheckKeyType()
109+
{
110+
$this->denormalizer->denormalize(
111+
[
112+
['foo' => 'one', 'bar' => 'two'],
113+
],
114+
__NAMESPACE__.'\ArrayDummy[]',
115+
null,
116+
['key_type' => new Type('string')]
117+
);
118+
}
119+
120+
/**
121+
* @expectedException \Symfony\Component\Serializer\Exception\NotNormalizableValueException
122+
*/
123+
public function testDenormalizeCheckKeyTypes()
124+
{
125+
$this->denormalizer->denormalize(
126+
[
127+
['foo' => 'one', 'bar' => 'two'],
128+
],
129+
__NAMESPACE__.'\ArrayDummy[]',
130+
null,
131+
['key_types' => [new Type('string')]]
132+
);
133+
}
134+
135+
public function testDenormalizeNestedCheckKeyTypes()
136+
{
137+
$m = $this->getMockBuilder(DenormalizerInterface::class)
138+
->getMock();
139+
140+
$denormalizer = new ArrayDenormalizer();
141+
$serializer = new Serializer([$denormalizer, $m]);
142+
$denormalizer->setSerializer($serializer);
143+
144+
$m->method('denormalize')
145+
->with(['foo' => 'one', 'bar' => 'two'], __NAMESPACE__.'\ArrayDummy')
146+
->willReturn(new ArrayDummy('one', 'two'));
147+
148+
$m->method('supportsDenormalization')
149+
->with($this->anything(), __NAMESPACE__.'\ArrayDummy')
150+
->willReturn(true);
151+
152+
$result = $denormalizer->denormalize(
153+
[
154+
'top' => [
155+
['foo' => 'one', 'bar' => 'two'],
156+
],
157+
],
158+
__NAMESPACE__.'\ArrayDummy[][]',
159+
null,
160+
['key_types' => [new Type('string')], 'key_type' => new Type('int')]
161+
);
162+
163+
$this->assertEquals(
164+
[
165+
'top' => [
166+
new ArrayDummy('one', 'two'),
167+
],
168+
],
169+
$result
170+
);
171+
}
172+
66173
public function testSupportsValidArray()
67174
{
68175
$this->serializer->expects($this->once())

0 commit comments

Comments
 (0)