8000 [Serializer] Add `SerializedPath` annotation to flatten nested attrib… · symfony/symfony@08a1119 · GitHub
[go: up one dir, main page]

Skip to content

Commit 08a1119

Browse files
boennernicolas-grekas
authored andcommitted
[Serializer] Add SerializedPath annotation to flatten nested attributes
1 parent c63bd7e commit 08a1119

25 files changed

+612
-34
lines changed

src/Symfony/Component/Serializer/Annotation/SerializedName.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ final class SerializedName
2727
{
2828
public function __construct(private string $serializedName)
2929
{
30-
if (empty($serializedName)) {
31-
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', static::class));
30+
if ('' === $serializedName) {
31+
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', self::class));
3232
}
3333
}
3434

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\Annotation;
13+
14+
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
15+
use Symfony\Component\PropertyAccess\PropertyPath;
16+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
17+
18+
/**
19+
* Annotation class for @SerializedPath().
20+
*
21+
* @Annotation
22+
* @NamedArgumentConstructor
23+
* @Target({"PROPERTY", "METHOD"})
24+
*
25+
* @author Tobias Bönner <tobi@boenner.family>
26+
*/
27+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)]
28+
final class SerializedPath
29+
{
30+
private PropertyPath $serializedPath;
31+
32+
public function __construct(string $serializedPath)
33+
{
34+
try {
35+
$this->serializedPath = new PropertyPath($serializedPath);
36+
} catch (InvalidPropertyPathException $pathException) {
37+
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a valid property path.', self::class));
38+
}
39+
}
40+
41+
public function getSerializedPath(): PropertyPath
42+
{
43+
return $this->serializedPath;
44+
}
45+
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)`
1212
* Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)`
1313
* 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.
14+
* Add `SerializedPath` annotation to flatten nested attributes
1415

1516
6.1
1617
---

src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Serializer\Mapping;
1313

14+
use Symfony\Component\PropertyAccess\PropertyPath;
15+
1416
/**
1517
* @author Kévin Dunglas <dunglas@gmail.com>
1618
*/
@@ -48,6 +50,13 @@ class AttributeMetadata implements AttributeMetadataInterface
4850
*/
4951
public $serializedName;
5052

53+
/**
54+
* @internal This property is public in order to reduce the size of the
55+
* class' serialized representation. Do not access it. Use
56+
* {@link getSerializedPath()} instead.
57+
*/
58+
public ?PropertyPath $serializedPath = null;
59+
5160
/**
5261
* @var bool
5362
*
@@ -121,6 +130,16 @@ public function getSerializedName(): ?string
121130
return $this->serializedName;
122131
}
123132

133+
public function setSerializedPath(PropertyPath $serializedPath = null): void
134+
{
135+
$this->serializedPath = $serializedPath;
136+
}
137+
138+
public function getSerializedPath(): ?PropertyPath
139+
{
140+
return $this->serializedPath;
141+
}
142+
124143
public function setIgnore(bool $ignore)
125144
{
126145
$this->ignore = $ignore;
@@ -190,14 +209,9 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
190209
}
191210

192211
// Overwrite only if not defined
193-
if (null === $this->maxDepth) {
194-
$this->maxDepth = $attributeMetadata< EED3 /span>->getMaxDepth();
195-
}
196-
197-
// Overwrite only if not defined
198-
if (null === $this->serializedName) {
199-
$this->serializedName = $attributeMetadata->getSerializedName();
200-
}
212+
$this->maxDepth ??= $attributeMetadata->getMaxDepth();
213+
$this->serializedName ??= $attributeMetadata->getSerializedName();
214+
$this->serializedPath ??= $attributeMetadata->getSerializedPath();
201215

202216
// Overwrite only if both contexts are empty
203217
if (!$this->normalizationContexts && !$this->denormalizationContexts) {
@@ -217,6 +231,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
217231
*/
218232
public function __sleep(): array
219233
{
220-
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
234+
return ['name', 'groups', 'maxDepth', 'serializedName', 'serializedPath', 'ignore', 'normalizationContexts', 'denormalizationContexts'];
221235
}
222236
}

src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Serializer\Mapping;
1313

14+
use Symfony\Component\PropertyAccess\PropertyPath;
15+
1416
/**
1517
* Stores metadata needed for serializing and deserializing attributes.
1618
*
@@ -59,6 +61,10 @@ public function setSerializedName(?string $serializedName);
5961
*/
6062
public function getSerializedName(): ?string;
6163

64+
public function setSerializedPath(?PropertyPath $serializedPath): void;
65+
66+
public function getSerializedPath(): ?PropertyPath;
67+
6268
/**
6369
* Sets if this attribute must be ignored or not.
6470
*/

src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ private function generateDeclaredClassMetadata(array $classMetadatas): string
4848
$attributeMetadata->getGroups(),
4949
$attributeMetadata->getMaxDepth(),
5050
$attributeMetadata->getSerializedName(),
51+
$attributeMetadata->getSerializedPath(),
5152
];
5253
}
5354

src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\Serializer\Annotation\Ignore;
1919
use Symfony\Component\Serializer\Annotation\MaxDepth;
2020
use Symfony\Component\Serializer\Annotation\SerializedName;
21+
use Symfony\Component\Serializer\Annotation\SerializedPath;
2122
use Symfony\Component\Serializer\Exception\MappingException;
2223
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
2324
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
@@ -38,6 +39,7 @@ class AnnotationLoader implements LoaderInterface
3839
Ignore::class,
3940
MaxDepth::class,
4041
SerializedName::class,
42+
SerializedPath::class,
4143
Context::class,
4244
];
4345

@@ -81,6 +83,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
8183
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
8284
} elseif ($annotation instanceof SerializedName) {
8385
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
86+
} elseif ($annotation instanceof SerializedPath) {
87+
$attributesMetadata[$property->name]->setSerializedPath($annotation->getSerializedPath());
8488
} elseif ($annotation instanceof Ignore) {
8589
$attributesMetadata[$property->name]->setIgnore(true);
8690
} elseif ($annotation instanceof Context) {
@@ -134,6 +138,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
134138
}
135139

136140
$attributeMetadata->setSerializedName($annotation->getSerializedName());
141+
} elseif ($annotation instanceof SerializedPath) {
142+
if (!$accessorOrMutator) {
143+
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));
144+
}
145+
146+
$attributeMetadata->setSerializedPath($annotation->getSerializedPath());
137147
} elseif ($annotation instanceof Ignore) {
138148
if (!$accessorOrMutator) {
139149
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));

src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Component\Serializer\Mapping\Loader;
1313

1414
use Symfony\Component\Config\Util\XmlUtils;
15+
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
16+
use Symfony\Component\PropertyAccess\PropertyPath;
1517
use Symfony\Component\Serializer\Exception\MappingException;
1618
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
1719
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
@@ -68,6 +70,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
6870
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
6971
}
7072

73+
if (isset($attribute['serialized-path'])) {
74+
try {
75+
$attributeMetadata->setSerializedPath(new PropertyPath((string) $attribute['serialized-path']));
76+
} catch (InvalidPropertyPathException) {
77+
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()));
78+
}
79+
}
80+
7181
if (isset($attribute['ignore'])) {
7282
$attributeMetadata->setIgnore(XmlUtils::phpize($attribute['ignore']));
7383
}

src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php

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

1212
namespace Symfony\Component\Serializer\Mapping\Loader;
1313

14+
use Symfony\Component\PropertyAccess\Exception\InvalidPropertyPathException;
15+
use Symfony\Component\PropertyAccess\PropertyPath;
1416
use Symfony\Component\Serializer\Exception\MappingException;
1517
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
1618
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
@@ -84,13 +86,21 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
8486
}
8587

8688
if (isset($data['serialized_name'])) {
87-
if (!\is_string($data['serialized_name']) || empty($data['serialized_name'])) {
89+
if (!\is_string($data['serialized_name']) || '' === $data['serialized_name']) {
8890
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()));
8991
}
9092

9193
$attributeMetadata->setSerializedName($data['serialized_name']);
9294
}
9395

96+
if (isset($data['serialized_path'])) {
97+
try {
98+
$attributeMetadata->setSerializedPath(new PropertyPath((string) $data['serialized_path']));
99+
} catch (InvalidPropertyPathException) {
100+
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()));
101+
}
102+
}
103+
94104
if (isset($data['ignore'])) {
95105
if (!\is_bool($data['ignore'])) {
96106
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()));

src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@
8181
</xsd:restriction>
8282
</xsd:simpleType>
8383
</xsd:attribute>
84+
<xsd:attribute name="serialized-path">
85+
<xsd:simpleType>
86+
<xsd:restriction base="xsd:string">
87+
<xsd:minLength value="1" />
88+
</xsd:restriction>
89+
</xsd:simpleType>
90+
</xsd:attribute>
8491
<xsd:attribute name="ignore" type="xsd:boolean" />
8592
</xsd:complexType>
8693

src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\NameConverter;
1313

14+
use Symfony\Component\Serializer\Exception\LogicException;
1415
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
1516
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
1617

@@ -76,6 +77,10 @@ private function getCacheValueForNormalization(string $propertyName, string $cla
7677
return null;
7778
}
7879

80+
if (null !== $attributesMetadata[$propertyName]->getSerializedName() && null !== $attributesMetadata[$propertyName]->getSerializedPath()) {
81+
throw new LogicException(sprintf('Found SerializedName and SerializedPath annotations on property "%s" of class "%s".', $propertyName, $class));
82+
}
83+
7984
return $attributesMetadata[$propertyName]->getSerializedName() ?? null;
8085
}
8186

@@ -113,6 +118,10 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex
113118
continue;
114119
}
115120

121+
if (null !== $metadata->getSerializedName() && null !== $metadata->getSerializedPath()) {
122+
throw new LogicException(sprintf('Found SerializedName and SerializedPath annotations on property "%s" of class "%s".', $name, $class));
123+
}
124+
116125
$groups = $metadata->getGroups();
117126
if (!$groups && ($context[AbstractNormalizer::GROUPS] ?? [])) {
118127
continue;

0 commit comments

Comments
 (0)
0