8000 feature #28505 [Serialized] allow configuring the serialized name of … · symfony/symfony@3e7b029 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3e7b029

Browse files
committed
feature #28505 [Serialized] allow configuring the serialized name of properties through metadata (fbourigault)
This PR was squashed before being merged into the 4.2-dev branch (closes #28505). Discussion ---------- [Serialized] allow configuring the serialized name of properties through metadata | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #15171 | License | MIT | Doc PR | symfony/symfony-docs#10422 This leverage the new `AdvancedNameConverterInterface` interface (#27021) to implement a name converter that relies on metadata. The name to use is configured per property using a `@SerializedName` annotation or the `serialized-name` XML attribute or the `serialized_name` key for YAML. This was exposed by @dunglas in #19374 (comment). # Framework integration For FramworkBundle integration, a ChainNameConverter could be added to allow users to use this name converter with a custom one. # To do - [x] add a CHANGELOG.md entry. - [x] add a fallback. - [x] add framework integration. - [x] add local caching to `MetadataAwareNameConverter`. - [x] add a doc PR. Commits ------- d1d1ceb [Serialized] allow configuring the serialized name of properties through metadata
2 parents 5a0cad2 + d1d1ceb commit 3e7b029

22 files changed

+516
-6
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@
7474
use Symfony\Component\PropertyAccess\PropertyAccessor;
7575
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
7676
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
77-
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
7877
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
78+
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
7979
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
8080
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
8181
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
@@ -1363,7 +1363,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
13631363
}
13641364

13651365
if (isset($config['name_converter']) && $config['name_converter']) {
1366-
$container->getDefinition('serializer.normalizer.object')->replaceArgument(1, new Reference($config['name_converter']));
1366+
$container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter']));
13671367
}
13681368

13691369
if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
<service id="serializer.normalizer.object" class="Symfony\Component\Serializer\Normalizer\ObjectNormalizer">
6060
<argument type="service" id="serializer.mapping.class_metadata_factory" />
61-
<argument>null</argument> <!-- name converter -->
61+
<argument type="service" id="serializer.name_converter.metadata_aware" />
6262
<argument type="service" id="serializer.property_accessor" />
6363
<argument type="service" id="property_info" on-invalid="ignore" />
6464
<argument type="service" id="serializer.mapping.class_discriminator_resolver" on-invalid="ignore" />
@@ -119,6 +119,10 @@
119119
<!-- Name converter -->
120120
<service id="serializer.name_converter.camel_case_to_snake_case" class="Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter" />
121121

122+
<service id="serializer.name_converter.metadata_aware" class="Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter" >
123+
<argument type="service" id="serializer.mapping.class_metadata_factory"/>
124+
</service>
125+
122126
<!-- PropertyInfo extractor -->
123127
<service id="property_info.serializer_extractor" class="Symfony\Component\PropertyInfo\Extractor\SerializerExtractor">
124128
<argument type="service" id="serializer.mapping.class_metadata_factory" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ public function testSerializerEnabled()
976976
$this->assertCount(2, $argument);
977977
$this->assertEquals('Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', $argument[0]->getClass());
978978
$this->assertNull($container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1));
979-
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.normalizer.object')->getArgument(1));
979+
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1));
980980
$this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3));
981981
$this->assertEquals(array('setCircularReferenceHandler', array(new Reference('my.circular.reference.handler'))), $container->getDefinition('serializer.normalizer.object')->getMethodCalls()[0]);
982982
$this->assertEquals(array('setMaxDepthHandler', array(new Reference('my.max.depth.handler'))), $container->getDefinition('serializer.normalizer.object')->getMethodCalls()[1]);

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"symfony/process": "~3.4|~4.0",
4646
"symfony/security-core": "~3.4|~4.0",
4747
"symfony/security-csrf": "~3.4|~4.0",
48-
"symfony/serializer": "^4.1",
48+
"symfony/serializer": "^4.2",
4949
"symfony/stopwatch": "~3.4|~4.0",
5050
"symfony/translation": "~4.2",
5151
"symfony/templating": "~3.4|~4.0",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\Serializer\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Annotation class for @SerializedName().
18+
*
19+
* @Annotation
20+
* @Target({"PROPERTY", "METHOD"})
21+
*
22+
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
23+
*/
24+
final class SerializedName
25+
{
26+
/**
27+
* @var string
28+
*/
29+
private $serializedName;
30+
31+
public function __construct(array $data)
32+
{
33+
if (!isset($data['value'])) {
34+
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', \get_class($this)));
35+
}
36+
37+
if (!\is_string($data['value']) || empty($data['value'])) {
38+
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', \get_class($this)));
39+
}
40+
41+
$this->serializedName = $data['value'];
42+
}
43+
44+
public function getSerializedName(): string
45+
{
46+
return $this->serializedName;
47+
}
48+
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ CHANGELOG
2222
either `EncoderInterface` or `DecoderInterface`
2323
* added the optional `$objectClassResolver` argument in `AbstractObjectNormalizer`
2424
and `ObjectNormalizer` constructor
25+
* added `MetadataAwareNameConverter` to configure the serialized name of properties through metadata
2526

2627
4.1.0
2728
-----

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ class AttributeMetadata implements AttributeMetadataInterface
4141
*/
4242
public $maxDepth;
4343

44+
/**
45+
* @var string|null
46+
*
47+
* @internal This property is public in order to reduce the size of the
48+
* class' serialized representation. Do not access it. Use
49+
* {@link getSerializedName()} instead.
50+
*/
51+
public $serializedName;
52+
4453
public function __construct(string $name)
4554
{
4655
$this->name = $name;
@@ -88,6 +97,22 @@ public function getMaxDepth()
8897
return $this->maxDepth;
8998
}
9099

100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function setSerializedName(string $serializedName = null)
104+
{
105+
$this->serializedName = $serializedName;
106+
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function getSerializedName(): ?string
112+
{
113+
return $this->serializedName;
114+
}
115+
91116
/**
92117
* {@inheritdoc}
93118
*/
@@ -101,6 +126,11 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
101126
if (null === $this->maxDepth) {
102127
$this->maxDepth = $attributeMetadata->getMaxDepth();
103128
}
129+
130+
// Overwrite only if not defined
131+
if (null === $this->serializedName) {
132+
$this->serializedName = $attributeMetadata->getSerializedName();
133+
}
104134
}
105135

106136
/**
@@ -110,6 +140,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
110140
*/
111141
public function __sleep()
112142
{
113-
return array('name', 'groups', 'maxDepth');
143+
return array('name', 'groups', 'maxDepth', 'serializedName');
114144
}
115145
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ public function setMaxDepth($maxDepth);
5757
*/
5858
public function getMaxDepth();
5959

60+
/**
61+
* Sets the serialization name for this attribute.
62+
*/
63+
public function setSerializedName(string $serializedName = null);
64+
65+
/**
66+
* Gets the serialization name for this attribute.
67+
*/
68+
public function getSerializedName(): ?string;
69+
6070
/**
6171
* Merges an {@see AttributeMetadataInterface} with in the current one.
6272
*/

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
1616
use Symfony\Component\Serializer\Annotation\Groups;
1717
use Symfony\Component\Serializer\Annotation\MaxDepth;
18+
use Symfony\Component\Serializer\Annotation\SerializedName;
1819
use Symfony\Component\Serializer\Exception\MappingException;
1920
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
2021
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
@@ -68,6 +69,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
6869
}
6970
} elseif ($annotation instanceof MaxDepth) {
7071
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
72+
} elseif ($annotation instanceof SerializedName) {
73+
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
7174
}
7275

7376
$loaded = true;
@@ -107,6 +110,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
107110
}
108111

109112
$attributeMetadata->setMaxDepth($annotation->getMaxDepth());
113+
} elseif ($annotation instanceof SerializedName) {
114+
if (!$accessorOrMutator) {
115+
throw new MappingException(sprintf('SerializedName on "%s::%s" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
116+
}
117+
118+
$attributeMetadata->setSerializedName($annotation->getSerializedName());
110119
}
111120

112121
$loaded = true;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
6666
if (isset($attribute['max-depth'])) {
6767
$attributeMetadata->setMaxDepth((int) $attribute['max-depth']);
6868
}
69+
70+
if (isset($attribute['serialized-name'])) {
71+
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
72+
}
6973
}
7074

7175
if (isset($xml->{'discriminator-map'})) {

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
8585

8686
$attributeMetadata->setMaxDepth($data['max_depth']);
8787
}
88+
89+
if (isset($data['serialized_name'])) {
90+
if (!\is_string($data['serialized_name']) || empty($data['serialized_name'])) {
91+
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()));
92+
}
93+
94+
$attributeMetadata->setSerializedName($data['serialized_name']);
95+
}
8896
}
8997
}
9098

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
@@ -71,6 +71,13 @@
7171
</xsd:restriction>
7272
</xsd:simpleType>
7373
</xsd:attribute>
74+
<xsd:attribute name="serialized-name">
75+
<xsd:simpleType>
76+
<xsd:restriction base="xsd:string">
77+
<xsd:minLength value="1" />
78+
</xsd:restriction>
79+
</xsd:simpleType>
80+
</xsd:attribute>
7481
</xsd:complexType>
7582

7683
</xsd:schema>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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\NameConverter;
13+
14+
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
15+
16+
/**
17+
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
18+
*/
19+
final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
20+
{
21+
private $metadataFactory;
22+
23+
/**
24+
* @var NameConverterInterface|AdvancedNameConverterInterface|null
25+
*/
26+
private $fallbackNameConverter;
27+
28+
private static $normalizeCache = array();
29+
30+
private static < 48DA span class=pl-c1>$denormalizeCache = array();
31+
32+
private static $attributesMetadataCache = array();
33+
34+
public function __construct(ClassMetadataFactoryInterface $metadataFactory, NameConverterInterface $fallbackNameConverter = null)
35+
{
36+
$this->metadataFactory = $metadataFactory;
37+
$this->fallbackNameConverter = $fallbackNameConverter;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function normalize($propertyName, string $class = null, string $format = null, array $context = array())
44+
{
45+
if (null === $class) {
46+
return $this->normalizeFallback($propertyName, $class, $format, $context);
47+
}
48+
49+
if (!isset(self::$normalizeCache[$class][$propertyName])) {
50+
self::$normalizeCache[$class][$propertyName] = $this->getCacheValueForNormalization($propertyName, $class);
51+
}
52+
53+
return self::$normalizeCache[$class][$propertyName] ?? $this->normalizeFallback($propertyName, $class, $format, $context);
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function denormalize($propertyName, string $class = null, string $format = null, array $context = array())
60+
{
61+
if (null === $class) {
62+
return $this->denormalizeFallback($propertyName, $class, $format, $context);
63+
}
64+
65+
if (!isset(self::$denormalizeCache[$class][$propertyName])) {
66+
self::$denormalizeCache[$class][$propertyName] = $this->getCacheValueForDenormalization($propertyName, $class);
67+
}
68+
69+
return self::$denormalizeCache[$class][$propertyName] ?? $this->denormalizeFallback($propertyName, $class, $format, $context);
70+
}
71+
72+
private function getCacheValueForNormalization(string $propertyName, string $class): ?string
73+
{
74+
if (!$this->metadataFactory->hasMetadataFor($class)) {
75+
return null;
76+
}
77+
78+
return $this->metadataFactory->getMetadataFor($class)->getAttributesMetadata()[$propertyName]->getSerializedName() ?? null;
79+
}
80+
81+
private function normalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = array()): string
82+
{
83+
return $this->fallbackNameConverter ? $this->fallbackNameConverter->normalize($propertyName, $class, $format, $context) : $propertyName;
84+
}
85+
86+
private function getCacheValueForDenormalization(string $propertyName, string $class): ?string
87+
{
88+
if (!isset(self::$attributesMetadataCache[$class])) {
89+
self::$attributesMetadataCache[$class] = $this->getCacheValueForAttributesMetadata($class);
90+
}
91+
92+
return self::$attributesMetadataCache[$class][$propertyName] ?? null;
93+
}
94+
95+
private function denormalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = array()): string
96+
{
97+
return $this->fallbackNameConverter ? $this->fallbackNameConverter->denormalize($propertyName, $class, $format, $context) : $propertyName;
98+
}
99+
100+
private function getCacheValueForAttributesMetadata(string $class): array
101+
{
102+
if (!$this->metadataFactory->hasMetadataFor($class)) {
103+
return array();
104+
}
105+
106+
$classMetadata = $this->metadataFactory->getMetadataFor($class);
107+
108+
$cache = array();
109+
foreach ($classMetadata->getAttributesMetadata() as $name => $metadata) {
110+
if (null === $metadata->getSerializedName()) {
111+
continue;
112+
}
113+
114+
$cache[$metadata->getSerializedName()] = $name;
115+
}
116+
117+
return $cache;
118+
}
119+
}

0 commit comments

Comments
 (0)
0