Skip to content

[Serializer] Serialization versioning #38478

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

Open
wants to merge 11 commits into
base: 7.3
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode)
->end()
->end()
->end()
->scalarNode('default_version')->end()

Choose a reason for hiding this comment

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

From #38542 serializer.default_context config option is avalable now.

Why not use serializer.default_context.version instead?

->end()
->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,12 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$defaultContext += ['max_depth_handler' => new Reference($config['max_depth_handler'])];
$container->getDefinition('serializer.normalizer.object')->replaceArgument(6, $defaultContext);
}

if (isset($config['default_version']) && $config['default_version']) {
$defaultContext = $container->getDefinition('serializer.normalizer.object')->getArgument(6);
$defaultContext += ['version' => $config['default_version']];
$container->getDefinition('serializer.normalizer.object')->replaceArgument(6, $defaultContext);
}
}

private function registerPropertyInfoConfiguration(ContainerBuilder $container, PhpFileLoader $loader)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
<xsd:attribute name="name-converter" type="xsd:string" />
<xsd:attribute name="circular-reference-handler" type="xsd:string" />
<xsd:attribute name="max-depth-handler" type="xsd:string" />
<xsd:attribute name="default-version" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="property_info">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
'name_converter' => 'serializer.name_converter.camel_case_to_snake_case',
'circular_reference_handler' => 'my.circular.reference.handler',
'max_depth_handler' => 'my.max.depth.handler',
'default_version' => '1.0.0',
],
'property_info' => true,
'ide' => 'file%%link%%format',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</framework:translator>
<framework:validation enabled="true" />
<framework:annotations cache="file" debug="true" file-cache-dir="%kernel.cache_dir%/annotations" />
<framework:serializer enabled="true" enable-annotations="true" name-converter="serializer.name_converter.camel_case_to_snake_case" circular-reference-handler="my.circular.reference.handler" max-depth-handler="my.max.depth.handler" />
<framework:serializer enabled="true" enable-annotations="true" name-converter="serializer.name_converter.camel_case_to_snake_case" circular-reference-handler="my.circular.reference.handler" max-depth-handler="my.max.depth.handler" default-version="1.0.0" />
<framework:property-info />
</framework:config>
</container>
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ framework:
name_converter: serializer.name_converter.camel_case_to_snake_case
circular_reference_handler: my.circular.reference.handler
max_depth_handler: my.max.depth.handler
default_version: 1.0.0
property_info: ~
ide: file%%link%%format
request:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,8 @@ public function testSerializerEnabled()
$this->assertArrayHasKey('circular_reference_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6));
$this->assertArrayHasKey('max_depth_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6));
$this->assertEquals($container->getDefinition('serializer.normalizer.object')->getArgument(6)['max_depth_handler'], new Reference('my.max.depth.handler'));
$this->assertArrayHasKey('version', $container->getDefinition('serializer.normalizer.object')->getArgument(6));
$this->assertEquals($container->getDefinition('serializer.normalizer.object')->getArgument(6)['version'], '1.0.0');
}

public function testRegisterSerializerExtractor()
Expand Down
48 changes: 48 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/Since.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 @Since().
Copy link
Contributor

@Seb33300 Seb33300 Aug 29, 2024

Choose a reason for hiding this comment

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

Not sure if you're still maintaining this PR, but this probably needs to be changed into an attribute now.

*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Arnaud Tarroux <ta.arnaud@gmail.com>
*/
class Since
{
/**
* @var string
*/
private $version;

public function __construct(array $data)
{
if (!isset($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', static::class));
}

if (!\is_string($data['value']) || empty($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', static::class));
}

$this->version = $data['value'];
}

public function getVersion(): string
{
return $this->version;
}
}
48 changes: 48 additions & 0 deletions src/Symfony/Component/Serializer/Annotation/Until.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 @Until().
*
* @Annotation
* @Target({"PROPERTY", "METHOD"})
*
* @author Arnaud Tarroux <ta.arnaud@gmail.com>
*/
class Until
{
/**
* @var string
*/
private $version;

public function __construct(array $data)
{
if (!isset($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" should be set.', static::class));
}

if (!\is_string($data['value']) || empty($data['value'])) {
throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a non-empty string.', static::class));
}

$this->version = $data['value'];
}

public function getVersion(): string
{
return $this->version;
}
}
52 changes: 51 additions & 1 deletion src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ class AttributeMetadata implements AttributeMetadataInterface
*/
public $ignore = false;

/**
* @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 getSince()} instead.
*/
public $since;

/**
* @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 getUntil()} instead.
*/
public $until;

public function __construct(string $name)
{
$this->name = $name;
Expand Down Expand Up @@ -162,13 +180,45 @@ public function merge(AttributeMetadataInterface $attributeMetadata)
}
}

/**
* {@inheritdoc}
*/
public function setSince(string $version)
{
$this->since = $version;
}

/**
* {@inheritdoc}
*/
public function getSince(): ?string
{
return $this->since;
}

/**
* {@inheritdoc}
*/
public function setUntil(string $version)
{
$this->until = $version;
}

/**
* {@inheritdoc}
*/
public function getUntil(): ?string
{
return $this->until;
}

/**
* Returns the names of the properties that should be serialized.
*
* @return string[]
*/
public function __sleep()
{
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore'];
return ['name', 'groups', 'maxDepth', 'serializedName', 'ignore', 'since', 'until'];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,24 @@ public function isIgnored(): bool;
* Merges an {@see AttributeMetadataInterface} with in the current one.
*/
public function merge(self $attributeMetadata);

/**
* Sets the version number from which the attribute must be serialized.
*/
public function setSince(string $version);

/**
* Gets the version number from which the attribute must be serialized.
*/
public function getSince(): ?string;

/**
* Sets the version number after which the attribute must not be serialized.
*/
public function setUntil(string $version);

/**
* Gets the version number after which the attribute must not be serialized.
*/
public function getUntil(): ?string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\Since;
use Symfony\Component\Serializer\Annotation\Until;
use Symfony\Component\Serializer\Exception\MappingException;
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
Expand Down Expand Up @@ -74,6 +76,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
$attributesMetadata[$property->name]->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributesMetadata[$property->name]->setIgnore(true);
} elseif ($annotation instanceof Since) {
$attributesMetadata[$property->name]->setSince($annotation->getVersion());
} elseif ($annotation instanceof Until) {
$attributesMetadata[$property->name]->setUntil($annotation->getVersion());
}

$loaded = true;
Expand Down Expand Up @@ -121,6 +127,18 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
$attributeMetadata->setSerializedName($annotation->getSerializedName());
} elseif ($annotation instanceof Ignore) {
$attributeMetadata->setIgnore(true);
} elseif ($annotation instanceof Since) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('Since on "%s::%s" cannot be added. Since can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}

$attributeMetadata->setSince($annotation->getVersion());
} elseif ($annotation instanceof Until) {
if (!$accessorOrMutator) {
throw new MappingException(sprintf('Until on "%s::%s" cannot be added. Until can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name));
}

$attributeMetadata->setUntil($annotation->getVersion());
}

$loaded = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)
if (isset($attribute['ignore'])) {
$attributeMetadata->setIgnore((bool) $attribute['ignore']);
}

if (isset($attribute['since'])) {
$attributeMetadata->setSince((string) $attribute['since']);
}

if (isset($attribute['until'])) {
$attributeMetadata->setUntil((string) $attribute['until']);
}
}

if (isset($xml->{'discriminator-map'})) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata)

$attributeMetadata->setIgnore($data['ignore']);
}

if (isset($data['since'])) {
$attributeMetadata->setSince((string) $data['since']);
}

if (isset($data['until'])) {
$attributeMetadata->setUntil((string) $data['until']);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="ignore" type="xsd:boolean" />
<xsd:attribute name="since">
<xsd:simpleType>
<xsd:restriction base="xsd:string">
<xsd:minLength value="1" />
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="until">
<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
Expand Up @@ -251,7 +251,8 @@ protected function getAllowedAttributes($classOrObject, array $context, bool $at
if (
!$ignore &&
(false === $groups || array_intersect(array_merge($attributeMetadata->getGroups(), ['*']), $groups)) &&
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context)
$this->isAllowedAttribute($classOrObject, $name = $attributeMetadata->getName(), null, $context) &&
$this->attributeAllowedWithVersion($context, $attributeMetadata->getSince(), $attributeMetadata->getUntil())
) {
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
}
Expand Down Expand Up @@ -447,4 +448,25 @@ protected function createChildContext(array $parentContext, string $attribute, ?

return $parentContext;
}

/**
* @internal
*/
private function attributeAllowedWithVersion(array $context, ?string $sinceVersion, ?string $untilVersion)
{
if (!isset($context['version']) && !isset($this->defaultContext['version'])) {
return true;
}

$version = $context['version'] ?? $this->defaultContext['version'];
if (null !== $sinceVersion && version_compare($version, $sinceVersion, '<')) {
return false;
}

if (null !== $untilVersion && version_compare($version, $untilVersion, '>=')) {
return false;
}

return true;
}
}
Loading