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

Skip to content

Commit 820c052

Browse files
committed
Fix denormalizing nested arrays as object values
1 parent 1a9c633 commit 820c052

File tree

4 files changed

+213
-21
lines changed

4 files changed

+213
-21
lines changed

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -397,23 +397,7 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
397397
return null;
398398
}
399399

400-
if ($type->isCollection() && null !== ($collectionValueType = $type->getCollectionValueType()) && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) {
401-
$builtinType = Type::BUILTIN_TYPE_OBJECT;
402-
$class = $collectionValueType->getClassName().'[]';
403-
404-
// Fix a collection that contains the only one element
405-
// This is special to xml format only
406-
if ('xml' === $format && !\is_int(key($data))) {
407-
$data = [$data];
408-
}
409-
410-
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
411-
$context['key_type'] = $collectionKeyType;
412-
}
413-
} else {
414-
$builtinType = $type->getBuiltinType();
415-
$class = $type->getClassName();
416-
}
400+
[$builtinType, $class] = $this->flattenDenormalizationType($type, $context);
417401

418402
$expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true;
419403

@@ -450,6 +434,29 @@ private function validateAndDenormalize(string $currentClass, string $attribute,
450434
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)));
451435
}
452436

437+
private function flattenDenormalizationType(Type $type, array &$context): array
438+
{
439+
if (!$type->isCollection() || !($collectionValueType = $type->getCollectionValueType()) || (Type::BUILTIN_TYPE_OBJECT !== $collectionValueType->getBuiltinType() && Type::BUILTIN_TYPE_ARRAY !== $collectionValueType->getBuiltinType())) {
440+
return [$type->getBuiltinType(), $type->getClassName()];
441+
}
442+
443+
if ($collectionValueType->isCollection()) {
444+
[$builtinType, $class] = $this->flattenDenormalizationType($collectionValueType, $context);
445+
} else {
446+
[$builtinType, $class] = [$collectionValueType->getBuiltinType(), $collectionValueType->getClassName()];
447+
}
448+
449+
if (Type::BUILTIN_TYPE_OBJECT === $builtinType) {
450+
$class = $class.'[]';
451+
452+
if (null !== $collectionKeyType = $type->getCollectionKeyType()) {
453+
$context['key_types'][] = $collectionKeyType;
454+
}
455+
}
456+
457+
return [$builtinType, $class];
458+
}
459+
453460
/**
454461
* @internal
455462
*/

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, $type, $format = null, array $context = [])
5152
$serializer = $this->serializer;
5253
$type = substr($type, 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: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
2626
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
2727
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
28+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
2829
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2930
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
3031
use Symfony\Component\Serializer\Serializer;
@@ -85,6 +86,31 @@ public function testDenormalizeWithExtraAttributesAndNoGroupsWithMetadataFactory
8586
);
8687
}
8788

89+
public function testDenormalizeDeepCollection()
90+
{
91+
$denormalizer = $this->getDenormalizerForDummyDeepCollection();
92+
93+
$dummyCollection = $denormalizer->denormalize(
94+
[
95+
'children' => [
96+
[
97+
[
98+
'bar' => 'first',
99+
],
100+
],
101+
],
102+
],
103+
DummyDeepCollection::class
104+
);
105+
106+
$this->assertInstanceOf(DummyDeepCollection::class, $dummyCollection);
107+
$this->assertInternalType('array', $dummyCollection->children);
108+
$this->assertCount(1, $dummyCollection->children);
109+
$this->assertInternalType('array', $dummyCollection->children[0]);
110+
$this->assertCount(1, $dummyCollection->children[0]);
111+
$this->assertInstanceOf(DummyChild::class, $dummyCollection->children[0][0]);
112+
}
113+
88114
public function testDenormalizeCollectionDecodedFromXmlWithOneChild()
89115
{
90116
$denormalizer = $this->getDenormalizerForDummyCollection();
@@ -100,7 +126,7 @@ public function testDenormalizeCollectionDecodedFromXmlWithOneChild()
100126
);
101127

102128
$this->assertInstanceOf(DummyCollection::class, $dummyCollection);
103-
$this->assertIsArray($dummyCollection->children);
129+
$this->assertInternalType('array', $dummyCollection->children);
104130
$this->assertCount(1, $dummyCollection->children);
105131
$this->assertInstanceOf(DummyChild::class, $dummyCollection->children[0]);
106132
}
@@ -121,7 +147,7 @@ public function testDenormalizeCollectionDecodedFromXmlWithTwoChildren()
121147
);
122148

123149
$this->assertInstanceOf(DummyCollection::class, $dummyCollection);
124-
$this->assertIsArray($dummyCollection->children);
150+
$this->assertInternalType('array', $dummyCollection->children);
125151
$this->assertCount(2, $dummyCollection->children);
126152
$this->assertInstanceOf(DummyChild::class, $dummyCollection->children[0]);
127153
$this->assertInstanceOf(DummyChild::class, $dummyCollection->children[1]);
@@ -146,7 +172,41 @@ private function getDenormalizerForDummyCollection()
146172
));
147173

148174
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
149-
$arrayDenormalizer = new ArrayDenormalizerDummy();
175+
$arrayDenormalizer = new ArrayDenormalizer();
176+
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
177+
$arrayDenormalizer->setSerializer($serializer);
178+
$denormalizer->setSerializer($serializer);
179+
180+
return $denormalizer;
181+
}
182+
183+
private function getDenormalizerForDummyDeepCollection()
184+
{
185+
$extractor = $this->getMockBuilder(PhpDocExtractor::class)->getMock();
186+
$extractor->method('getTypes')
187+
->will($this->onConsecutiveCalls(
188+
[
189+
new Type(
190+
'array',
191+
false,
192+
null,
193+
true,
194+
new Type('int'),
195+
new Type(
196+
'array',
197+
false,
198+
null,
199+
true,
200+
new Type('int'),
201+
new Type('object', false, DummyChild::class)
202+
)
203+
),
204+
],
205+
null
206+
));
207+
208+
$denormalizer = new AbstractObjectNormalizerCollectionDummy(null, null, $extractor);
209+
$arrayDenormalizer = new ArrayDenormalizer();
150210
$serializer = new SerializerCollectionDummy([$arrayDenormalizer, $denormalizer]);
151211
$arrayDenormalizer->setSerializer($serializer);
152212
$denormalizer->setSerializer($serializer);
@@ -264,6 +324,12 @@ class DummyCollection
264324
public $children;
265325
}
266326

327+
class DummyDeepCollection
328+
{
329+
/** @var DummyChild[][] */
330+
public $children;
331+
}
332+
267333
class DummyChild
268334
{
269335
public $bar;

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
use PHPUnit\Framework\MockObject\MockObject;
1515
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\PropertyInfo\Type;
1617
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
18+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
19+
use Symfony\Component\Serializer\Serializer;
1720
use Symfony\Component\Serializer\SerializerInterface;
1821

1922
class ArrayDenormalizerTest extends TestCase
@@ -64,6 +67,110 @@ public function testDenormalize()
6467
);
6568
}
6669

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

0 commit comments

Comments
 (0)