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

Skip to content

Commit 71f8c57

Browse files
bug #50193 [Serializer] Fix SerializedPath not working with constructor arguments (HypeMC)
This PR was merged into the 6.2 branch. Discussion ---------- [Serializer] Fix `SerializedPath` not working with constructor arguments | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Currently the `#[SerializedPath]` attribute doesn't work with constructor arguments: ```php class Test { public function __construct( #[SerializedPath('[foo][bar]')] public string $bar, ) { } } $serializer->denormalize(['foo' => ['bar' => 'something']], Test::class); ``` ``` In AbstractNormalizer.php line 384: Cannot create an instance of "Test" from serialized data because its constructor requires parameter "bar" to be present. ``` Commits ------- 240c031 [Serializer] Fix SerializedPath not working with constructor arguments
2 parents a689085 + 240c031 commit 71f8c57

10 files changed

+298
-35
lines changed

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

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
128128

129129
$this->propertyTypeExtractor = $propertyTypeExtractor;
130130

131-
if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) {
132-
$classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
131+
if ($classMetadataFactory) {
132+
$classDiscriminatorResolver ??= new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
133133
}
134134
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
135135
$this->objectClassResolver = $objectClassResolver;
@@ -217,7 +217,7 @@ public function normalize(mixed $object, string $format = null, array $context =
217217
}
218218

219219
$preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false;
220-
if ($preserveEmptyObjects && !\count($data)) {
220+
if ($preserveEmptyObjects && !$data) {
221221
return new \ArrayObject();
222222
}
223223

@@ -226,19 +226,8 @@ public function normalize(mixed $object, string $format = null, array $context =
226226

227227
protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null)
228228
{
229-
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
230-
if (!isset($data[$mapping->getTypeProperty()])) {
231-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
232-
}
233-
234-
$type = $data[$mapping->getTypeProperty()];
235-
if (null === ($mappedClass = $mapping->getClassForType($type))) {
236-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
237-
}
238-
239-
if ($mappedClass !== $class) {
240-
return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
241-
}
229+
if ($class !== $mappedClass = $this->getMappedClass($data, $class, $context)) {
230+
return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
242231
}
243232

244233
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
@@ -270,7 +259,7 @@ protected function getAttributes(object $object, ?string $format, array $context
270259

271260
$attributes = $this->extractAttributes($object, $format, $context);
272261

273-
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) {
262+
if ($mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object)) {
274263
array_unshift($attributes, $mapping->getTypeProperty());
275264
}
276265

@@ -319,11 +308,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
319308
$normalizedData = $this->prepareForDenormalization($data);
320309
$extraAttributes = [];
321310

322-
$reflectionClass = new \ReflectionClass($type);
323-
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
324-
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class;
311+
$mappedClass = $this->getMappedClass($normalizedData, $type, $context);
325312

326-
$nestedAttributes = $this->getNestedAttributes($resolvedClass);
313+
$nestedAttributes = $this->getNestedAttributes($mappedClass);
327314
$nestedData = [];
328315
$propertyAccessor = PropertyAccess::createPropertyAccessor();
329316
foreach ($nestedAttributes as $property => $serializedPath) {
@@ -336,6 +323,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
336323

337324
$normalizedData = array_merge($normalizedData, $nestedData);
338325

326+
$object = $this->instantiateObject($normalizedData, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
327+
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class;
328+
339329
foreach ($normalizedData as $attribute => $value) {
340330
if ($this->nameConverter) {
341331
$notConverted = $attribute;
@@ -675,11 +665,8 @@ private function updateData(array $data, string $attribute, mixed $attributeValu
675665
*/
676666
private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool
677667
{
678-
$enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false;
679-
if (
680-
!$enableMaxDepth ||
681-
!isset($attributesMetadata[$attribute]) ||
682-
null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth()
668+
if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false)
669+
|| null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth()
683670
) {
684671
return false;
685672
}
@@ -755,7 +742,7 @@ private function isUninitializedValueError(\Error $e): bool
755742
*/
756743
private function getNestedAttributes(string $class): array
757744
{
758-
if (!$this->classMetadataFactory || !$this->classMetadataFactory->hasMetadataFor($class)) {
745+
if (!$this->classMetadataFactory?->hasMetadataFor($class)) {
759746
return [];
760747
}
761748

@@ -781,15 +768,30 @@ private function getNestedAttributes(string $class): array
781768
private function removeNestedValue(array $path, array $data): array
782769
{
783770
$element = array_shift($path);
784-
if ([] === $path) {
771+
if (!$path || !$data[$element] = $this->removeNestedValue($path, $data[$element])) {
785772
unset($data[$element]);
786-
} else {
787-
$data[$element] = $this->removeNestedValue($path, $data[$element]);
788-
if ([] === $data[$element]) {
789-
unset($data[$element]);
790-
}
791773
}
792774

793775
return $data;
794776
}
777+
778+
/**
779+
* @return class-string
780+
*/
781+
private function getMappedClass(array $data, string $class, array $context): string
782+
{
783+
if (!$mapping = $this->classDiscriminatorResolver?->getMappingForClass($class)) {
784+
return $class;
785+
}
786+
787+
if (null === $type = $data[$mapping->getTypeProperty()] ?? null) {
788+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
789+
}
790+
791+
if (null === $mappedClass = $mapping->getClassForType($type)) {
792+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
793+
}
794+
795+
return $mappedClass;
796+
}
795797
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Fixtures\Annotations;
13+
14+
use Symfony\Component\Serializer\Annotation\SerializedPath;
15+
16+
class SerializedPathInConstructorDummy
17+
{
18+
public function __construct(
19+
/**
20+
* @SerializedPath("[one][two]")
21+
*/
22+
public $three,
23+
) {
24+
}
25+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Fixtures\Attributes;
13+
14+
use Symfony\Component\Serializer\Annotation\SerializedPath;
15+
16+
class SerializedPathInConstructorDummy
17+
{
18+
public function __construct(
19+
#[SerializedPath('[one][two]')] public $three,
20+
) {
21+
}
22+
}

src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
<attribute name="seven" serialized-path="[three][four]" />
3131
</class>
3232

33+
<class name="Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy">
34+
<attribute name="three" serialized-path="[one][two]" />
35+
</class>
36+
3337
<class name="Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy">
3438
<discriminator-map type-property="type">
3539
<mapping type="first" class="Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummyFirstChild" />

src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
serialized_path: '[one][two]'
2323
seven:
2424
serialized_path: '[three][four]'
25+
'Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy':
26+
attributes:
27+
three:
28+
serialized_path: '[one][two]'
2529
'Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy':
2630
discriminator_map:
2731
type_property: type

src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\MaxDepthDummy;
2020
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedNameDummy;
2121
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy;
22+
use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy;
2223
use Symfony\Component\Serializer\Tests\Fixtures\Dummy;
2324

2425
final class ClassMetadataFactoryCompilerTest extends TestCase
@@ -46,18 +47,20 @@ public function testItDumpMetadata()
4647
$maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class);
4748
$serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class);
4849
$serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class);
50+
$serializedPathInConstructorDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathInConstructorDummy::class);
4951

5052
$code = (new ClassMetadataFactoryCompiler())->compile([
5153
$dummyMetadata,
5254
$maxDepthDummyMetadata,
5355
$serializedNameDummyMetadata,
5456
$serializedPathDummyMetadata,
57+
$serializedPathInConstructorDummyMetadata,
5558
]);
5659

5760
file_put_contents($this->dumpPath, $code);
5861
$compiledMetadata = require $this->dumpPath;
5962

60-
$this->assertCount(4, $compiledMetadata);
63+
$this->assertCount(5, $compiledMetadata);
6164

6265
$this->assertArrayHasKey(Dummy::class, $compiledMetadata);
6366
$this->assertEquals([
@@ -99,5 +102,13 @@ public function testItDumpMetadata()
99102
],
100103
null,
101104
], $compiledMetadata[SerializedPathDummy::class]);
105+
106+
$this->assertArrayHasKey(SerializedPathInConstructorDummy::class, $compiledMetadata);
107+
$this->assertEquals([
108+
[
109+
'three' => [[], null, null, '[one][two]'],
110+
],
111+
null,
112+
], $compiledMetadata[SerializedPathInConstructorDummy::class]);
102113
}
103114
}

src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ public function testLoadSerializedPath()
103103
$this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath());
104104
}
105105

106+
public function testLoadSerializedPathInConstructor()
107+
{
108+
$classMetadata = new ClassMetadata($this->getNamespace().'\SerializedPathInConstructorDummy');
109+
$this->loader->loadClassMetadata($classMetadata);
110+
111+
$attributesMetadata = $classMetadata->getAttributesMetadata();
112+
$this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath());
113+
}
114+
106115
public function testLoadClassMetadataAndMerge()
107116
{
108117
$classMetadata = new ClassMetadata($this->getNamespace().'\GroupDummy');

src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ public function testSerializedPath()
9494
$this->assertEquals('[three][four]', $attributesMetadata['seven']->getSerializedPath());
9595
}
9696

97+
public function testSerializedPathInConstructor()
98+
{
99+
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy');
100+
$this->loader->loadClassMetadata($classMetadata);
101+
102+
$attributesMetadata = $classMetadata->getAttributesMetadata();
103+
$this->assertEquals('[one][two]', $attributesMetadata['three']->getSerializedPath());
104+
}
105+
97106
public function testLoadDiscriminatorMap()
98107
{
99108
$classMetadata = new ClassMetadata(AbstractDummy::class);

src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ public function testSerializedPath()
108108
$this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath());
109109
}
110110

111+
public function testSerializedPathInConstructor()
112+
{
113+
$classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy');
114+
$this->loader->loadClassMetadata($classMetadata);
115+
116+
$attributesMetadata = $classMetadata->getAttributesMetadata();
117+
$this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath());
118+
}
119+
111120
public function testLoadDiscriminatorMap()
112121
{
113122
$classMetadata = new ClassMetadata(AbstractDummy::class);

0 commit comments

Comments
 (0)