diff --git a/src/Symfony/Component/Serializer/Annotation/SerializedName.php b/src/Symfony/Component/Serializer/Annotation/SerializedName.php index 93438cdfb7370..b6c6027e8568e 100644 --- a/src/Symfony/Component/Serializer/Annotation/SerializedName.php +++ b/src/Symfony/Component/Serializer/Annotation/SerializedName.php @@ -27,8 +27,8 @@ final class SerializedName { public function __construct(private string $serializedName) { - if (empty($serializedName)) { - throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', static::class)); + if ('' === $serializedName) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', self::class)); } } diff --git a/src/Symfony/Component/Serializer/Annotation/SerializedPath.php b/src/Symfony/Component/Serializer/Annotation/SerializedPath.php new file mode 100644 index 0000000000000..88ee8e47d76b1 --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/SerializedPath.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Annotation; + +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; + +/** + * Annotation class for @SerializedPath(). + * + * @Annotation + * @NamedArgumentConstructor + * @Target({"PROPERTY", "METHOD"}) + * + * @author Tobias Bönner + */ +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] +final class SerializedPath +{ + private PropertyPath $serializedPath; + + public function __construct(string $serializedPath) + { + try { + $this->serializedPath = new PropertyPath($serializedPath); + } catch (InvalidPropertyPathException $pathException) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a valid property path.', self::class)); + } + } + + public function getSerializedPath(): PropertyPath + { + return $this->serializedPath; + } +} diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 21cbb4256d9c1..3342ada2fea86 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)` * Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)` * Add option YamlEncoder::YAML_INDENTATION to YamlEncoder constructor options to configure additional indentation for each level of nesting. This allows configuring indentation in the service configuration. + * Add `SerializedPath` annotation to flatten nested attributes 6.1 --- diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php index 180909b9f7b30..22cc711a7e2c8 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Mapping; +use Symfony\Component\PropertyAccess\PropertyPath; + /** * @author Kévin Dunglas */ @@ -48,6 +50,13 @@ class AttributeMetadata implements AttributeMetadataInterface */ public $serializedName; + /** + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getSerializedPath()} instead. + */ + public ?PropertyPath $serializedPath = null; + /** * @var bool * @@ -121,6 +130,16 @@ public function getSerializedName(): ?string return $this->serializedName; } + public function setSerializedPath(PropertyPath $serializedPath = null): void + { + $this->serializedPath = $serializedPath; + } + + public function getSerializedPath(): ?PropertyPath + { + return $this->serializedPath; + } + public function setIgnore(bool $ignore) { $this->ignore = $ignore; @@ -190,14 +209,9 @@ public function merge(AttributeMetadataInterface $attributeMetadata) } // Overwrite only if not defined - if (null === $this->maxDepth) { - $this->maxDepth = $attributeMetadata->getMaxDepth(); - } - - // Overwrite only if not defined - if (null === $this->serializedName) { - $this->serializedName = $attributeMetadata->getSerializedName(); - } + $this->maxDepth ??= $attributeMetadata->getMaxDepth(); + $this->serializedName ??= $attributeMetadata->getSerializedName(); + $this->serializedPath ??= $attributeMetadata->getSerializedPath(); // Overwrite only if both contexts are empty if (!$this->normalizationContexts && !$this->denormalizationContexts) { @@ -217,6 +231,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata) */ public function __sleep(): array { - return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore', 'normalizationContexts', 'denormalizationContexts']; + return ['name', 'groups', 'maxDepth', 'serializedName', 'serializedPath', 'ignore', 'normalizationContexts', 'denormalizationContexts']; } } diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php index 6a215e0ea4299..67ca8d3c0631c 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Mapping; +use Symfony\Component\PropertyAccess\PropertyPath; + /** * Stores metadata needed for serializing and deserializing attributes. * @@ -59,6 +61,10 @@ public function setSerializedName(?string $serializedName); */ public function getSerializedName(): ?string; + public function setSerializedPath(?PropertyPath $serializedPath): void; + + public function getSerializedPath(): ?PropertyPath; + /** * Sets if this attribute must be ignored or not. */ diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php index 81dd4b9323bab..f01fe9ce2f085 100644 --- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -48,6 +48,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string $attributeMetadata->getGroups(), $attributeMetadata->getMaxDepth(), $attributeMetadata->getSerializedName(), + $attributeMetadata->getSerializedPath(), ]; } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index 25d4beaf699f6..cfcee8bd5013d 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -18,6 +18,7 @@ use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Serializer\Annotation\SerializedPath; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; @@ -38,6 +39,7 @@ class AnnotationLoader implements LoaderInterface Ignore::class, MaxDepth::class, SerializedName::class, + SerializedPath::class, Context::class, ]; @@ -81,6 +83,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth()); } elseif ($annotation instanceof SerializedName) { $attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName()); + } elseif ($annotation instanceof SerializedPath) { + $attributesMetadata[$property->name]->setSerializedPath($annotation->getSerializedPath()); } elseif ($annotation instanceof Ignore) { $attributesMetadata[$property->name]->setIgnore(true); } elseif ($annotation instanceof Context) { @@ -134,6 +138,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } $attributeMetadata->setSerializedName($annotation->getSerializedName()); + } elseif ($annotation instanceof SerializedPath) { + if (!$accessorOrMutator) { + throw new MappingException(sprintf('SerializedPath on "%s::%s()" cannot be added. SerializedPath can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + } + + $attributeMetadata->setSerializedPath($annotation->getSerializedPath()); } elseif ($annotation instanceof Ignore) { if (!$accessorOrMutator) { throw new MappingException(sprintf('Ignore on "%s::%s()" cannot be added. Ignore can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index 6d6ae16a9957c..3dc3b96c69c94 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer\Mapping\Loader; use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; @@ -68,6 +70,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool $attributeMetadata->setSerializedName((string) $attribute['serialized-name']); } + if (isset($attribute['serialized-path'])) { + try { + $attributeMetadata->setSerializedPath(new PropertyPath((string) $attribute['serialized-path'])); + } catch (InvalidPropertyPathException) { + throw new MappingException(sprintf('The "serialized-path" value must be a valid property path for the attribute "%s" of the class "%s".', $attributeName, $classMetadata->getName())); + } + } + if (isset($attribute['ignore'])) { $attributeMetadata->setIgnore(XmlUtils::phpize($attribute['ignore'])); } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 907d3384411c3..0fdfcc511093a 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Mapping\Loader; +use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; @@ -84,13 +86,21 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool } if (isset($data['serialized_name'])) { - if (!\is_string($data['serialized_name']) || empty($data['serialized_name'])) { + if (!\is_string($data['serialized_name']) || '' === $data['serialized_name']) { throw new MappingException(sprintf('The "serialized_name" value must be a non-empty string in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); } $attributeMetadata->setSerializedName($data['serialized_name']); } + if (isset($data['serialized_path'])) { + try { + $attributeMetadata->setSerializedPath(new PropertyPath((string) $data['serialized_path'])); + } catch (InvalidPropertyPathException) { + throw new MappingException(sprintf('The "serialized_path" value must be a valid property path in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); + } + } + if (isset($data['ignore'])) { if (!\is_bool($data['ignore'])) { throw new MappingException(sprintf('The "ignore" value must be a boolean in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName())); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index 0228e41ce10d3..f5f6cca9f0f54 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -81,6 +81,13 @@ + + + + + + + diff --git a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php index 24c9991e10581..920e81869561e 100644 --- a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php +++ b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\NameConverter; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -76,6 +77,10 @@ private function getCacheValueForNormalization(string $propertyName, string $cla return null; } + if (null !== $attributesMetadata[$propertyName]->getSerializedName() && null !== $attributesMetadata[$propertyName]->getSerializedPath()) { + throw new LogicException(sprintf('Found SerializedName and SerializedPath annotations on property "%s" of class "%s".', $propertyName, $class)); + } + return $attributesMetadata[$propertyName]->getSerializedName() ?? null; } @@ -113,6 +118,10 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex continue; } + if (null !== $metadata->getSerializedName() && null !== $metadata->getSerializedPath()) { + throw new LogicException(sprintf('Found SerializedName and SerializedPath annotations on property "%s" of class "%s".', $name, $class)); + } + $groups = $metadata->getGroups(); if (!$groups && ($context[AbstractNormalizer::GROUPS] ?? [])) { continue; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index febe70bb47a9c..7c4c5fb41bd49 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -14,6 +14,7 @@ use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Encoder\CsvEncoder; @@ -26,6 +27,7 @@ use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -157,6 +159,7 @@ public function normalize(mixed $object, string $format = null, array $context = $stack = []; $attributes = $this->getAttributes($object, $format, $context); $class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + $classMetadata = $this->classMetadataFactory?->getMetadataFor($class); $attributesMetadata = $this->classMetadataFactory?->getMetadataFor($class)->getAttributesMetadata(); if (isset($context[self::MAX_DEPTH_HANDLER])) { $maxDepthHandler = $context[self::MAX_DEPTH_HANDLER]; @@ -199,7 +202,7 @@ public function normalize(mixed $object, string $format = null, array $context = $stack[$attribute] = $attributeValue; } - $data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext); + $data = $this->updateData($data, $attribute, $attributeValue, $class, $format, $attributeContext, $attributesMetadata, $classMetadata); } foreach ($stack as $attribute => $attributeValue) { @@ -210,7 +213,7 @@ public function normalize(mixed $object, string $format = null, array $context = $attributeContext = $this->getAttributeNormalizationContext($object, $attribute, $context); $childContext = $this->createChildContext($attributeContext, $attribute, $format); - $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext); + $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $childContext), $class, $format, $attributeContext, $attributesMetadata, $classMetadata); } $preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false; @@ -320,9 +323,26 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format); $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + $nestedAttributes = $this->getNestedAttributes($resolvedClass); + $nestedData = []; + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + foreach ($nestedAttributes as $property => $serializedPath) { + if (null === $value = $propertyAccessor->getValue($normalizedData, $serializedPath)) { + continue; + } + $nestedData[$property] = $value; + $normalizedData = $this->removeNestedValue($serializedPath->getElements(), $normalizedData); + } + + $normalizedData = array_merge($normalizedData, $nestedData); + foreach ($normalizedData as $attribute => $value) { if ($this->nameConverter) { + $notConverted = $attribute; $attribute = $this->nameConverter->denormalize($attribute, $resolvedClass, $format, $context); + if (isset($nestedData[$notConverted]) && !isset($nestedData[$attribute])) { + throw new LogicException(sprintf('Duplicate values for key "%s" found. One value is set via the SerializedPath annotation: "%s", the other one is set via the SerializedName annotation: "%s".', $notConverted, implode('->', $nestedAttributes[$notConverted]->getElements()), $attribute)); + } } $attributeContext = $this->getAttributeDenormalizationContext($resolvedClass, $attribute, $context); @@ -623,12 +643,22 @@ private function getTypes(string $currentClass, string $attribute): ?array /** * Sets an attribute and apply the name converter if necessary. */ - private function updateData(array $data, string $attribute, mixed $attributeValue, string $class, ?string $format, array $context): array + private function updateData(array $data, string $attribute, mixed $attributeValue, string $class, ?string $format, array $context, ?array $attributesMetadata, ?ClassMetadataInterface $classMetadata): array { if (null === $attributeValue && ($context[self::SKIP_NULL_VALUES] ?? $this->defaultContext[self::SKIP_NULL_VALUES] ?? false)) { return $data; } + if (null !== $classMetadata && null !== $serializedPath = ($attributesMetadata[$attribute] ?? null)?->getSerializedPath()) { + $propertyAccessor = PropertyAccess::createPropertyAccessor(); + if ($propertyAccessor->isReadable($data, $serializedPath) && null !== $propertyAccessor->getValue($data, $serializedPath)) { + throw new LogicException(sprintf('The element you are trying to set is already populated: "%s".', (string) $serializedPath)); + } + $propertyAccessor->setValue($data, $serializedPath, $attributeValue); + + return $data; + } + if ($this->nameConverter) { $attribute = $this->nameConverter->normalize($attribute, $class, $format, $context); } @@ -719,4 +749,47 @@ private function isUninitializedValueError(\Error $e): bool return str_starts_with($e->getMessage(), 'Typed property') && str_ends_with($e->getMessage(), 'must not be accessed before initialization'); } + + /** + * Returns all attributes with a SerializedPath annotation and the respective path. + */ + private function getNestedAttributes(string $class): array + { + if (!$this->classMetadataFactory || !$this->classMetadataFactory->hasMetadataFor($class)) { + return []; + } + + $properties = []; + $serializedPaths = []; + $classMetadata = $this->classMetadataFactory->getMetadataFor($class); + foreach ($classMetadata->getAttributesMetadata() as $name => $metadata) { + if (!$serializedPath = $metadata->getSerializedPath()) { + continue; + } + $serializedPath = $metadata->getSerializedPath(); + $pathIdentifier = implode(',', $serializedPath->getElements()); + if (isset($serializedPaths[$pathIdentifier])) { + throw new LogicException(sprintf('Duplicate serialized path: "%s" used for properties "%s" and "%s".', $pathIdentifier, $serializedPaths[$pathIdentifier], $name)); + } + $serializedPaths[$pathIdentifier] = $name; + $properties[$name] = $serializedPath; + } + + return $properties; + } + + private function removeNestedValue(array $path, array $data): array + { + $element = array_shift($path); + if ([] === $path) { + unset($data[$element]); + } else { + $data[$element] = $this->removeNestedValue($path, $data[$element]); + if ([] === $data[$element]) { + unset($data[$element]); + } + } + + return $data; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php index 4e81ad59de21a..f4dd82d7fad9b 100644 --- a/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php +++ b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php @@ -20,16 +20,12 @@ */ class SerializedNameTest extends TestCase { - /** - * @testWith [""] - * [0] - */ - public function testNotAStringSerializedNameParameter($value) + public function testNotAStringSerializedNameParameter() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\SerializedName" must be a non-empty string.'); - new SerializedName($value); + new SerializedName(''); } public function testSerializedNameParameters() diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/SerializedPathTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedPathTest.php new file mode 100644 index 0000000000000..58a057bd31e1c --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Annotation/SerializedPathTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Annotation; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\Serializer\Annotation\SerializedPath; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; + +/** + * @author Tobias Bönner + */ +class SerializedPathTest extends TestCase +{ + public function testEmptyStringSerializedPathParameter() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Parameter of annotation "Symfony\Component\Serializer\Annotation\SerializedPath" must be a valid property path.'); + + new SerializedPath(''); + } + + public function testSerializedPath() + { + $path = '[one][two]'; + $serializedPath = new SerializedPath($path); + $propertyPath = new PropertyPath($path); + $this->assertEquals($propertyPath, $serializedPath->getSerializedPath()); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathDummy.php new file mode 100644 index 0000000000000..cd50b81c3372e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathDummy.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +/** + * @author Tobias Bönner + */ +class SerializedPathDummy +{ + /** + * @SerializedPath("[one][two]") + */ + public $three; + + public $seven; + + /** + * @SerializedPath("[three][four]") + */ + public function getSeven() + { + return $this->seven; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathDummy.php new file mode 100644 index 0000000000000..fc5d9f64ab2d0 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathDummy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +/** + * @author Tobias Bönner + */ +class SerializedPathDummy +{ + #[SerializedPath('[one][two]')] + public $three; + + public $seven; + + #[SerializedPath('[three][four]')] + public function getSeven() + { + return $this->seven; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml index 69243cfddb5ae..4890f56bfd0f9 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml @@ -25,6 +25,11 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index 80100e8260622..7519b979efa96 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -16,6 +16,12 @@ serialized_name: 'baz' bar: serialized_name: 'qux' +'Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy': + attributes: + three: + serialized_path: '[one][two]' + seven: + serialized_path: '[three][four]' 'Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy': discriminator_map: type_property: type diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php index 8fc4b8b49865c..1f1b291beb7f0 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Mapping; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; @@ -58,6 +59,15 @@ public function testSerializedName() $this->assertEquals('serialized_name', $attributeMetadata->getSerializedName()); } + public function testSerializedPath() + { + $attributeMetadata = new AttributeMetadata('path'); + $serializedPath = new PropertyPath('[serialized][path]'); + $attributeMetadata->setSerializedPath($serializedPath); + + $this->assertEquals($serializedPath, $attributeMetadata->getSerializedPath()); + } + public function testIgnore() { $attributeMetadata = new AttributeMetadata('ignored'); @@ -119,6 +129,7 @@ public function testGetContextsForGroups() public function testMerge() { + $serializedPath = new PropertyPath('[a4][a5]'); $attributeMetadata1 = new AttributeMetadata('a1'); $attributeMetadata1->addGroup('a'); $attributeMetadata1->addGroup('b'); @@ -128,6 +139,7 @@ public function testMerge() $attributeMetadata2->addGroup('c'); $attributeMetadata2->setMaxDepth(2); $attributeMetadata2->setSerializedName('a3'); + $attributeMetadata2->setSerializedPath($serializedPath); $attributeMetadata2->setNormalizationContextForGroups(['foo' => 'bar'], ['a']); $attributeMetadata2->setDenormalizationContextForGroups(['baz' => 'qux'], ['c']); @@ -138,6 +150,7 @@ public function testMerge() $this->assertEquals(['a', 'b', 'c'], $attributeMetadata1->getGroups()); $this->assertEquals(2, $attributeMetadata1->getMaxDepth()); $this->assertEquals('a3', $attributeMetadata1->getSerializedName()); + $this->assertEquals($serializedPath, $attributeMetadata1->getSerializedPath()); $this->assertSame(['a' => ['foo' => 'bar']], $attributeMetadata1->getNormalizationContexts()); $this->assertSame(['c' => ['baz' => 'qux']], $attributeMetadata1->getDenormalizationContexts()); $this->assertTrue($attributeMetadata1->isIgnored()); @@ -166,6 +179,8 @@ public function testSerialize() $attributeMetadata->addGroup('b'); $attributeMetadata->setMaxDepth(3); $attributeMetadata->setSerializedName('serialized_name'); + $serializedPath = new PropertyPath('[serialized][path]'); + $attributeMetadata->setSerializedPath($serializedPath); $serialized = serialize($attributeMetadata); $this->assertEquals($attributeMetadata, unserialize($serialized)); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 6d562e30f57fd..5ce1931ba0cab 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedNameDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; final class ClassMetadataFactoryCompilerTest extends TestCase @@ -44,25 +45,27 @@ public function testItDumpMetadata() $dummyMetadata = $classMetatadataFactory->getMetadataFor(Dummy::class); $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); + $serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class); $code = (new ClassMetadataFactoryCompiler())->compile([ $dummyMetadata, $maxDepthDummyMetadata, $serializedNameDummyMetadata, + $serializedPathDummyMetadata, ]); file_put_contents($this->dumpPath, $code); $compiledMetadata = require $this->dumpPath; - $this->assertCount(3, $compiledMetadata); + $this->assertCount(4, $compiledMetadata); $this->assertArrayHasKey(Dummy::class, $compiledMetadata); $this->assertEquals([ [ - 'foo' => [[], null, null], - 'bar' => [[], null, null], - 'baz' => [[], null, null], - 'qux' => [[], null, null], + 'foo' => [[], null, null, null], + 'bar' => [[], null, null, null], + 'baz' => [[], null, null, null], + 'qux' => [[], null, null, null], ], null, ], $compiledMetadata[Dummy::class]); @@ -70,9 +73,9 @@ public function testItDumpMetadata() $this->assertArrayHasKey(MaxDepthDummy::class, $compiledMetadata); $this->assertEquals([ [ - 'foo' => [[], 2, null], - 'bar' => [[], 3, null], - 'child' => [[], null, null], + 'foo' => [[], 2, null, null], + 'bar' => [[], 3, null, null], + 'child' => [[], null, null, null], ], null, ], $compiledMetadata[MaxDepthDummy::class]); @@ -80,12 +83,21 @@ public function testItDumpMetadata() $this->assertArrayHasKey(SerializedNameDummy::class, $compiledMetadata); $this->assertEquals([ [ - 'foo' => [[], null, 'baz'], - 'bar' => [[], null, 'qux'], - 'quux' => [[], null, null], - 'child' => [[], null, null], + 'foo' => [[], null, 'baz', null], + 'bar' => [[], null, 'qux', null], + 'quux' => [[], null, null, null], + 'child' => [[], null, null, null], ], null, ], $compiledMetadata[SerializedNameDummy::class]); + + $this->assertArrayHasKey(SerializedPathDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'three' => [[], null, null, '[one][two]'], + 'seven' => [[], null, null, '[three][four]'], + ], + null, + ], $compiledMetadata[SerializedPathDummy::class]); } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index 0df08f9dc12de..0747ca3f54c7a 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; @@ -92,6 +93,16 @@ public function testLoadSerializedName() $this->assertEquals('qux', $attributesMetadata['bar']->getSerializedName()); } + public function testLoadSerializedPath() + { + $classMetadata = new ClassMetadata($this->getNamespace().'\SerializedPathDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); + } + public function testLoadClassMetadataAndMerge() { $classMetadata = new ClassMetadata($this->getNamespace().'\GroupDummy'); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index 47d6305a898f2..b1e9ed7222636 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -84,6 +84,16 @@ public function testSerializedName() $this->assertEquals('qux', $attributesMetadata['bar']->getSerializedName()); } + public function testSerializedPath() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals('[one][two]', $attributesMetadata['three']->getSerializedPath()); + $this->assertEquals('[three][four]', $attributesMetadata['seven']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index aa235762bdeb5..bbe0a99aeab89 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Loader; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; @@ -97,6 +98,16 @@ public function testSerializedName() $this->assertEquals('qux', $attributesMetadata['bar']->getSerializedName()); } + public function testSerializedPath() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php index 119edfbfb954d..6cd01bbc17540 100644 --- a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php +++ b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php @@ -13,6 +13,9 @@ use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Serializer\Annotation\SerializedPath; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -163,4 +166,31 @@ public function testDenormalizeWithCacheContext() $this->assertEquals('buzForExport', $nameConverter->denormalize('buz', OtherSerializedNameDummy::class, null, ['groups' => ['b']])); $this->assertEquals('buz', $nameConverter->denormalize('buz', OtherSerializedNameDummy::class)); } + + public function testDenormalizeWithNestedPathAndName() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Found SerializedName and SerializedPath annotations on property "foo" of class "Symfony\Component\Serializer\Tests\NameConverter\NestedPathAndName".'); + $nameConverter->denormalize('foo', NestedPathAndName::class); + } + + public function testNormalizeWithNestedPathAndName() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Found SerializedName and SerializedPath annotations on property "foo" of class "Symfony\Component\Serializer\Tests\NameConverter\NestedPathAndName".'); + $nameConverter->normalize('foo', NestedPathAndName::class); + } +} + +class NestedPathAndName +{ + /** + * @SerializedName("five") + * @SerializedPath("[one][two][three]") + */ + public $foo; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index b34d40d692b8b..9c8ba3ffcf4e2 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -16,6 +16,8 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Serializer\Annotation\SerializedPath; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -28,6 +30,7 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -103,6 +106,132 @@ public function testDenormalizeWithExtraAttributesAndNoGroupsWithMetadataFactory ); } + public function testDenormalizeWithDuplicateNestedAttributes() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate serialized path: "one,two,three" used for properties "foo" and "bar".'); + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $normalizer->denormalize([], DuplicateValueNestedDummy::class, 'any'); + } + + public function testDenormalizeWithNestedAttributesWithoutMetadata() + { + $normalizer = new AbstractObjectNormalizerDummy(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + $test = $normalizer->denormalize($data, NestedDummy::class, 'any'); + $this->assertSame('notfoo', $test->foo); + $this->assertSame('baz', $test->baz); + $this->assertNull($test->notfoo); + } + + public function testDenormalizeWithNestedAttributes() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + $test = $normalizer->denormalize($data, NestedDummy::class, 'any'); + $this->assertSame('baz', $test->baz); + $this->assertSame('foo', $test->foo); + $this->assertSame('quux', $test->quux); + $this->assertSame('notfoo', $test->notfoo); + } + + public function testDenormalizeWithNestedAttributesDuplicateKeys() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Duplicate values for key "quux" found. One value is set via the SerializedPath annotation: "one->four", the other one is set via the SerializedName annotation: "notquux".'); + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'four' => 'quux', + ], + 'quux' => 'notquux', + ]; + $normalizer->denormalize($data, DuplicateKeyNestedDummy::class, 'any'); + } + + public function testNormalizeWithNestedAttributesMixingArrayTypes() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two]"'); + $foobar = new AlreadyPopulatedNestedDummy(); + $foobar->foo = 'foo'; + $foobar->bar = 'bar'; + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $normalizer->normalize($foobar, 'any'); + } + + public function testNormalizeWithNestedAttributesElementAlreadySet() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The element you are trying to set is already populated: "[one][two][three]"'); + $foobar = new DuplicateValueNestedDummy(); + $foobar->foo = 'foo'; + $foobar->bar = 'bar'; + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $normalizer->normalize($foobar, 'any'); + } + + public function testNormalizeWithNestedAttributes() + { + $foobar = new NestedDummy(); + $foobar->foo = 'foo'; + $foobar->quux = 'quux'; + $foobar->baz = 'baz'; + $foobar->notfoo = 'notfoo'; + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + $test = $normalizer->normalize($foobar, 'any'); + $this->assertSame($data, $test); + } + + public function testNormalizeWithNestedAttributesWithoutMetadata() + { + $foobar = new NestedDummy(); + $foobar->foo = 'foo'; + $foobar->quux = 'quux'; + $foobar->baz = 'baz'; + $foobar->notfoo = 'notfoo'; + $data = [ + 'foo' => 'foo', + 'quux' => 'quux', + 'notfoo' => 'notfoo', + 'baz' => 'baz', + ]; + $normalizer = new ObjectNormalizer(); + $test = $normalizer->normalize($foobar, 'any'); + $this->assertSame($data, $test); + } + public function testDenormalizeCollectionDecodedFromXmlWithOneChild() { $denormalizer = $this->getDenormalizerForDummyCollection(); @@ -436,11 +565,73 @@ class EmptyDummy { } +class AlreadyPopulatedNestedDummy +{ + /** + * @SerializedPath("[one][two][three]") + */ + public $foo; + + /** + * @SerializedPath("[one][two]") + */ + public $bar; +} + +class DuplicateValueNestedDummy +{ + /** + * @SerializedPath("[one][two][three]") + */ + public $foo; + + /** + * @SerializedPath("[one][two][three]") + */ + public $bar; + + public $baz; +} + +class NestedDummy +{ + /** + * @SerializedPath("[one][two][three]") + */ + public $foo; + + /** + * @SerializedPath("[one][four]") + */ + public $quux; + + /** + * @SerializedPath("[foo]") + */ + public $notfoo; + + public $baz; +} + +class DuplicateKeyNestedDummy +{ + /** + * @SerializedPath("[one][four]") + */ + public $quux; + + /** + * @SerializedName("quux") + */ + public $notquux; +} + class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer { public function __construct() { - parent::__construct(new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()))); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + parent::__construct($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); } protected function extractAttributes(object $object, string $format = null, array $context = []): array