Skip to content

[Serialized] allow configuring the serialized name of properties through metadata #28505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'm ok to bump this dependency. But feature detection may be used to support both versions. WDYT @symfony/deciders?

"symfony/stopwatch": "~3.4|~4.0",
"symfony/translation": "~4.2",
"symfony/templating": "~3.4|~4.0",
Expand Down
48 changes: 48 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/SerializedName.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
32 changes: 31 additions & 1 deletion src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it still really necessary to make this property public? We do that everywhere in validator (and I copied this behavior in Serializer), but I'm not sure that it's still accurate.
Anyway, keep it here for consistency.


public function __construct(string $name)
{
$this->name = $name;
Expand Down Expand Up @@ -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}
*/
Expand All @@ -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();
}
}

/**
Expand All @@ -110,6 +140,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
*/
public function __sleep()
{
return array('name', 'groups', 'maxDepth');
return array('name', 'groups', 'maxDepth', 'serializedName');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ public function setMaxDepth($maxDepth);
*/
public function getMaxDepth();

/**
* Sets the serialization name for this attribute.
*/
public function setSerializedName(string $serializedName = null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BC break? why not otherwise?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, internal interface.


/**
* Gets the serialization name for this attribute.
*/
public function getSerializedName(): ?string;

/**
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'})) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading