Skip to content

Commit bde5221

Browse files
feature symfony#61532 [Serializer] Allow using attributes to declare compile-time serialization metadata (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [Serializer] Allow using attributes to declare compile-time serialization metadata | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Issues | - | License | MIT Very close to symfony#61528 Prerequisite for symfony#61287 At the moment, serialization attributes are read at runtime when `framework.serialization.enable_attributes` is true. This means they don't fit for bundles nor can't they be warmed up. This PR fixes both issues by using a new `serializer.attribute_metadata` resource tag, that's turned into a list of classes to parse for attributes at compile-time. For bundles and for apps, the tag is added by explicit service configuration: ```php ->set('my_bundle.api_resource.product', Product::class) ->resourceTag('serializer.attribute_metadata') ``` Unlike validation where we have constraint attributes to auto-discover service resources, serialization doesn't have any corresponding hooks. We do have a few like `#[DiscriminatorMap]` of `#[Groups]`, but relying on those would miss many more classes that are meant for serialization. Maybe we could introduce an attribute that'd hint that some class is serializable by the component (and require the attribute at some point in the future?) Commits ------- bc6e054 [Serializer] Allow using attributes to declare compile-time serialization metadata
2 parents c539c77 + bc6e054 commit bde5221

File tree

18 files changed

+240
-21
lines changed

18 files changed

+240
-21
lines changed

UPGRADE-7.4.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Serializer
9595
* Make `AttributeMetadata` and `ClassMetadata` final
9696
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
9797
* Deprecate getters in attribute classes in favor of public properties
98+
* Deprecate `ClassMetadataFactoryCompiler`
9899

99100
String
100101
------

src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1515
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
1616
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
17+
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
1718
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
1819
use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface;
1920
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
2021
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
2122

2223
/**
23-
* Warms up XML and YAML serializer metadata.
24+
* Warms up serializer metadata.
2425
*
2526
* @author Titouan Galopin <[email protected]>
2627
*
@@ -66,14 +67,14 @@ protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?strin
6667
/**
6768
* @param LoaderInterface[] $loaders
6869
*
69-
* @return XmlFileLoader[]|YamlFileLoader[]
70+
* @return list<XmlFileLoader|YamlFileLoader|AttributeLoader>
7071
*/
7172
private function extractSupportedLoaders(array $loaders): array
7273
{
7374
$supportedLoaders = [];
7475

7576
foreach ($loaders as $loader) {
76-
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
77+
if (method_exists($loader, 'getMappedClasses')) {
7778
$supportedLoaders[] = $loader;
7879
} elseif ($loader instanceof LoaderChain) {
7980
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders()));

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@
187187
use Symfony\Component\Semaphore\Semaphore;
188188
use Symfony\Component\Semaphore\SemaphoreFactory;
189189
use Symfony\Component\Semaphore\Store\StoreFactory as SemaphoreStoreFactory;
190+
use Symfony\Component\Serializer\Attribute as SerializerMapping;
191+
use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass;
190192
use Symfony\Component\Serializer\Encoder\DecoderInterface;
191193
use Symfony\Component\Serializer\Encoder\EncoderInterface;
192-
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
193194
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
194195
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
195196
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
@@ -2073,10 +2074,38 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
20732074
}
20742075

20752076
$serializerLoaders = [];
2076-
if ($config['enable_attributes'] ?? false) {
2077-
$attributeLoader = new Definition(AttributeLoader::class);
20782077

2079-
$serializerLoaders[] = $attributeLoader;
2078+
// When attributes are disabled, it means from runtime-discovery only; autoconfiguration should still happen.
2079+
// And when runtime-discovery of attributes is enabled, we can skip compile-time autoconfiguration in debug mode.
2080+
if (class_exists(SerializerAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug'))) {
2081+
// The $reflector argument hints at where the attribute could be used
2082+
$configurator = function (ChildDefinition $definition, object $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) {
2083+
$definition->addTag('serializer.attribute_metadata');
2084+
};
2085+
$container->registerAttributeForAutoconfiguration(SerializerMapping\Context::class, $configurator);
2086+
$container->registerAttributeForAutoconfiguration(SerializerMapping\Groups::class, $configurator);
2087+
2088+
$configurator = function (ChildDefinition $definition, object $attribute, \ReflectionMethod|\ReflectionProperty $reflector) {
2089+
$definition->addTag('serializer.attribute_metadata');
2090+
};
2091+
$container->registerAttributeForAutoconfiguration(SerializerMapping\Ignore::class, $configurator);
2092+
$container->registerAttributeForAutoconfiguration(SerializerMapping\MaxDepth::class, $configurator);
2093+
$container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedName::class, $configurator);
2094+
$container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedPath::class, $configurator);
2095+
2096+
$container->registerAttributeForAutoconfiguration(SerializerMapping\DiscriminatorMap::class, function (ChildDefinition $definition) {
2097+
$definition->addTag('serializer.attribute_metadata');
2098+
});
2099+
}
2100+
2101+
if (($config['enable_attributes'] ?? false) || class_exists(SerializerAttributeMetadataPass::class)) {
2102+
$serializerLoaders[] = new Reference('serializer.mapping.attribute_loader');
2103+
2104+
$container->getDefinition('serializer.mapping.attribute_loader')
2105+
->replaceArgument(0, $config['enable_attributes'] ?? false);
2106+
} else {
2107+
// BC with symfony/serializer < 7.4
2108+
$container->removeDefinition('serializer.mapping.attribute_services_loader');
20802109
}
20812110

20822111
$fileRecorder = function ($extension, $path) use (&$serializerLoaders) {

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
6666
use Symfony\Component\Runtime\SymfonyRuntime;
6767
use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass;
68+
use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass;
6869
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
6970
use Symfony\Component\Translation\DependencyInjection\DataCollectorTranslatorPass;
7071
use Symfony\Component\Translation\DependencyInjection\LoggingTranslatorPass;
@@ -170,6 +171,7 @@ public function build(ContainerBuilder $container): void
170171
$this->addCompilerPassIfExists($container, TranslationDumperPass::class);
171172
$container->addCompilerPass(new FragmentRendererPass());
172173
$this->addCompilerPassIfExists($container, SerializerPass::class);
174+
$this->addCompilerPassIfExists($container, SerializerAttributeMetadataPass::class);
173175
$this->addCompilerPassIfExists($container, PropertyInfoPass::class);
174176
$this->addCompilerPassIfExists($container, PropertyInfoConstructorPass::class);
175177
$container->addCompilerPass(new ControllerArgumentValueResolverPass());

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory;
2929
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
3030
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
31+
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
3132
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
3233
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
3334
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
@@ -151,6 +152,9 @@
151152
->set('serializer.mapping.chain_loader', LoaderChain::class)
152153
->args([[]])
153154

155+
->set('serializer.mapping.attribute_loader', AttributeLoader::class)
156+
->args([true, []])
157+
154158
// Class Metadata Factory
155159
->set('serializer.mapping.class_metadata_factory', ClassMetadataFactory::class)
156160
->args([service('serializer.mapping.chain_loader')])

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'handle_all_throwables' => true,
77
'php_errors' => ['log' => true],
88
'serializer' => [
9-
'enable_attributes' => false,
9+
'enable_attributes' => true,
1010
'mapping' => [
1111
'paths' => [
1212
'%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files',

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<framework:config http-method-override="false" handle-all-throwables="true">
88
<framework:annotations enabled="false" />
99
<framework:php-errors log="true" />
10-
<framework:serializer enable-attributes="false">
10+
<framework:serializer enable-attributes="true">
1111
<framework:mapping>
1212
<framework:path>%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files</framework:path>
1313
<framework:path>%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/serialization.yml</framework:path>

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ framework:
55
php_errors:
66
log: true
77
serializer:
8-
enable_attributes: false
8+
enable_attributes: true
99
mapping:
1010
paths:
1111
- "%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files"

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
use Symfony\Component\PropertyAccess\PropertyAccessor;
7878
use Symfony\Component\Security\Core\AuthenticationEvents;
7979
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
80-
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
8180
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
8281
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
8382
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
@@ -1571,7 +1570,7 @@ public function testSerializerEnabled()
15711570
$argument = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0);
15721571

15731572
$this->assertCount(2, $argument);
1574-
$this->assertEquals(AttributeLoader::class, $argument[0]->getClass());
1573+
$this->assertEquals(new Reference('serializer.mapping.attribute_loader'), $argument[0]);
15751574
$this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1));
15761575
$this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3));
15771576
}
@@ -1761,6 +1760,7 @@ public function testSerializerMapping()
17611760
$projectDir = $container->getParameter('kernel.project_dir');
17621761
$configDir = __DIR__.'/Fixtures/TestBundle/Resources/config';
17631762
$expectedLoaders = [
1763+
new Reference('serializer.mapping.attribute_loader'),
17641764
new Definition(XmlFileLoader::class, [$configDir.'/serialization.xml']),
17651765
new Definition(YamlFileLoader::class, [$configDir.'/serialization.yml']),
17661766
new Definition(YamlFileLoader::class, [$projectDir.'/config/serializer/foo.yml']),
@@ -1770,15 +1770,15 @@ public function testSerializerMapping()
17701770
new Definition(YamlFileLoader::class, [$configDir.'/serializer_mapping/serialization.yaml']),
17711771
];
17721772

1773-
foreach ($expectedLoaders as $definition) {
1774-
if (is_file($arg = $definition->getArgument(0))) {
1775-
$definition->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR));
1773+
foreach ($expectedLoaders as $loader) {
1774+
if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) {
1775+
$loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR));
17761776
}
17771777
}
17781778

17791779
$loaders = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0);
17801780
foreach ($loaders as $loader) {
1781-
if (is_file($arg = $loader->getArgument(0))) {
1781+
if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) {
17821782
$loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR));
17831783
}
17841784
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ CHANGELOG
44
7.4
55
---
66

7+
* Add `AttributeMetadataPass` to declare compile-time constraint metadata using attributes
78
* Add `CDATA_WRAPPING_NAME_PATTERN` support to `XmlEncoder`
89
* Add support for `can*()` methods to `AttributeLoader`
910
* Make `AttributeMetadata` and `ClassMetadata` final
1011
* Deprecate class aliases in the `Annotation` namespace, use attributes instead
1112
* Deprecate getters in attribute classes in favor of public properties
13+
* Deprecate `ClassMetadataFactoryCompiler`
1214

1315
7.3
1416
---

0 commit comments

Comments
 (0)