Permalink
Browse files

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
  • Loading branch information...
dunglas committed Oct 5, 2018
2 parents 5a0cad2 + d1d1ceb commit 3e7b029524bf724abfd603c17795af39bb289615
Showing with 516 additions and 6 deletions.
  1. +2 −2 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
  2. +5 −1 src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml
  3. +1 −1 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php
  4. +1 −1 src/Symfony/Bundle/FrameworkBundle/composer.json
  5. +48 −0 src/Symfony/Component/Serializer/Annotation/SerializedName.php
  6. +1 −0 src/Symfony/Component/Serializer/CHANGELOG.md
  7. +31 −1 src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
  8. +10 −0 src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php
  9. +9 −0 src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php
  10. +4 −0 src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php
  11. +8 −0 src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php
  12. +7 −0 ...fony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd
  13. +119 −0 src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php
  14. +55 −0 src/Symfony/Component/Serializer/Tests/Annotation/SerializedNameTest.php
  15. +47 −0 src/Symfony/Component/Serializer/Tests/Fixtures/SerializedNameDummy.php
  16. +5 −0 src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml
  17. +6 −0 src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml
  18. +11 −0 src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php
  19. +10 −0 src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php
  20. +10 −0 src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php
  21. +10 −0 src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php
  22. +116 −0 src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php
@@ -74,8 +74,8 @@
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
@@ -1363,7 +1363,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
}
if (isset($config['name_converter']) && $config['name_converter']) {
$container->getDefinition('serializer.normalizer.object')->replaceArgument(1, new Reference($config['name_converter']));
$container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter']));
}
if (isset($config['circular_reference_handler']) && $config['circular_reference_handler']) {
@@ -58,7 +58,7 @@
<service id="serializer.normalizer.object" class="Symfony\Component\Serializer\Normalizer\ObjectNormalizer">
<argument type="service" id="serializer.mapping.class_metadata_factory" />
<argument>null</argument> <!-- name converter -->
<argument type="service" id="serializer.name_converter.metadata_aware" />
<argument type="service" id="serializer.property_accessor" />
<argument type="service" id="property_info" on-invalid="ignore" />
<argument type="service" id="serializer.mapping.class_discriminator_resolver" on-invalid="ignore" />
@@ -119,6 +119,10 @@
<!-- Name converter -->
<service id="serializer.name_converter.camel_case_to_snake_case" class="Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter" />
<service id="serializer.name_converter.metadata_aware" class="Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter" >
<argument type="service" id="serializer.mapping.class_metadata_factory"/>
</service>
<!-- PropertyInfo extractor -->
<service id="property_info.serializer_extractor" class="Symfony\Component\PropertyInfo\Extractor\SerializerExtractor">
<argument type="service" id="serializer.mapping.class_metadata_factory" />
@@ -976,7 +976,7 @@ public function testSerializerEnabled()
$this->assertCount(2, $argument);
$this->assertEquals('Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', $argument[0]->getClass());
$this->assertNull($container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1));
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.normalizer.object')->getArgument(1));
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1));
$this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3));
$this->assertEquals(array('setCircularReferenceHandler', array(new Reference('my.circular.reference.handler'))), $container->getDefinition('serializer.normalizer.object')->getMethodCalls()[0]);
$this->assertEquals(array('setMaxDepthHandler', array(new Reference('my.max.depth.handler'))), $container->getDefinition('serializer.normalizer.object')->getMethodCalls()[1]);
@@ -45,7 +45,7 @@
"symfony/process": "~3.4|~4.0",
"symfony/security-core": "~3.4|~4.0",
"symfony/security-csrf": "~3.4|~4.0",
"symfony/serializer": "^4.1",
"symfony/serializer": "^4.2",
"symfony/stopwatch": "~3.4|~4.0",
"symfony/translation": "~4.2",
"symfony/templating": "~3.4|~4.0",
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Serializer\Exception\InvalidArgumentException;
/**
* Annotation class for @SerializedName().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class SerializedName
{
/**
* @var string
*/
private $serializedName;
public function __construct(array $data)
{
if (!isset($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', \get_class($this)));
}
if (!\is_string($data['value']) || empty($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', \get_class($this)));
}
$this->serializedName = $data['value'];
}
public function getSerializedName(): string
{
return $this->serializedName;
}
}
@@ -22,6 +22,7 @@ CHANGELOG
either `EncoderInterface` or `DecoderInterface`
* added the optional `$objectClassResolver` argument in `AbstractObjectNormalizer`
and `ObjectNormalizer` constructor
* added `MetadataAwareNameConverter` to configure the serialized name of properties through metadata
4.1.0
-----
@@ -41,6 +41,15 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public $maxDepth;
/**
* @var string|null
*
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getSerializedName()} instead.
*/
public $serializedName;
public function __construct(string $name)
{
$this->name = $name;
@@ -88,6 +97,22 @@ public function getMaxDepth()
return $this->maxDepth;
}
/**
* {@inheritdoc}
*/
public function setSerializedName(string $serializedName = null)
{
$this->serializedName = $serializedName;
}
/**
* {@inheritdoc}
*/
public function getSerializedName(): ?string
{
return $this->serializedName;
}
/**
* {@inheritdoc}
*/
@@ -101,6 +126,11 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
if (null === $this->maxDepth) {
$this->maxDepth = $attributeMetadata->getMaxDepth();
}
// Overwrite only if not defined
if (null === $this->serializedName) {
$this->serializedName = $attributeMetadata->getSerializedName();
}
}
/**
@@ -110,6 +140,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
*/
public function __sleep()
{
return array('name', 'groups', 'maxDepth');
return array('name', 'groups', 'maxDepth', 'serializedName');
}
}
@@ -57,6 +57,16 @@ public function setMaxDepth($maxDepth);
*/
public function getMaxDepth();
/**
* Sets the serialization name for this attribute.
*/
public function setSerializedName(string $serializedName = null);
/**
* Gets the serialization name for this attribute.
*/
public function getSerializedName(): ?string;
/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
@@ -15,6 +15,7 @@
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
@@ -68,6 +69,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
}
} elseif ($annotation instanceof MaxDepth) {
$attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
}
$loaded = true;
@@ -107,6 +110,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
}
$attributeMetadata->setMaxDepth($annotation->getMaxDepth());
} elseif ($annotation instanceof SerializedName) {
if (!$accessorOrMutator) {
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));
}
$attributeMetadata->setSerializedName($annotation->getSerializedName());
}
$loaded = true;
@@ -66,6 +66,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
if (isset($attribute['max-depth'])) {
$attributeMetadata->setMaxDepth((int) $attribute['max-depth']);
}
if (isset($attribute['serialized-name'])) {
$attributeMetadata->setSerializedName((string) $attribute['serialized-name']);
}
}
if (isset($xml->{'discriminator-map'})) {
@@ -85,6 +85,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
$attributeMetadata->setMaxDepth($data['max_depth']);
}
if (isset($data['serialized_name'])) {
if (!\is_string($data['serialized_name']) || empty($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']);
}
}
}
@@ -71,6 +71,13 @@
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="serialized-name">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:schema>
@@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Serializer\NameConverter;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
/**
* @author Fabien Bourigault <bourigaultfabien@gmail.com>
*/
final class MetadataAwareNameConverter implements AdvancedNameConverterInterface
{
private $metadataFactory;
/**
* @var NameConverterInterface|AdvancedNameConverterInterface|null
*/
private $fallbackNameConverter;
private static $normalizeCache = array();
private static $denormalizeCache = array();
private static $attributesMetadataCache = array();
public function __construct(ClassMetadataFactoryInterface $metadataFactory, NameConverterInterface $fallbackNameConverter = null)
{
$this->metadataFactory = $metadataFactory;
$this->fallbackNameConverter = $fallbackNameConverter;
}
/**
* {@inheritdoc}
*/
public function normalize($propertyName, string $class = null, string $format = null, array $context = array())
{
if (null === $class) {
return $this->normalizeFallback($propertyName, $class, $format, $context);
}
if (!isset(self::$normalizeCache[$class][$propertyName])) {
self::$normalizeCache[$class][$propertyName] = $this->getCacheValueForNormalization($propertyName, $class);
}
return self::$normalizeCache[$class][$propertyName] ?? $this->normalizeFallback($propertyName, $class, $format, $context);
}
/**
* {@inheritdoc}
*/
public function denormalize($propertyName, string $class = null, string $format = null, array $context = array())
{
if (null === $class) {
return $this->denormalizeFallback($propertyName, $class, $format, $context);
}
if (!isset(self::$denormalizeCache[$class][$propertyName])) {
self::$denormalizeCache[$class][$propertyName] = $this->getCacheValueForDenormalization($propertyName, $class);
}
return self::$denormalizeCache[$class][$propertyName] ?? $this->denormalizeFallback($propertyName, $class, $format, $context);
}
private function getCacheValueForNormalization(string $propertyName, string $class): ?string
{
if (!$this->metadataFactory->hasMetadataFor($class)) {
return null;
}
return $this->metadataFactory->getMetadataFor($class)->getAttributesMetadata()[$propertyName]->getSerializedName() ?? null;
}
private function normalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = array()): string
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->normalize($propertyName, $class, $format, $context) : $propertyName;
}
private function getCacheValueForDenormalization(string $propertyName, string $class): ?string
{
if (!isset(self::$attributesMetadataCache[$class])) {
self::$attributesMetadataCache[$class] = $this->getCacheValueForAttributesMetadata($class);
}
return self::$attributesMetadataCache[$class][$propertyName] ?? null;
}
private function denormalizeFallback(string $propertyName, string $class = null, string $format = null, array $context = array()): string
{
return $this->fallbackNameConverter ? $this->fallbackNameConverter->denormalize($propertyName, $class, $format, $context) : $propertyName;
}
private function getCacheValueForAttributesMetadata(string $class): array
{
if (!$this->metadataFactory->hasMetadataFor($class)) {
return array();
}
$classMetadata = $this->metadataFactory->getMetadataFor($class);
$cache = array();
foreach ($classMetadata->getAttributesMetadata() as $name => $metadata) {
if (null === $metadata->getSerializedName()) {
continue;
}
$cache[$metadata->getSerializedName()] = $name;
}
return $cache;
}
}
Oops, something went wrong.

0 comments on commit 3e7b029

Please sign in to comment.