From bc6e054e355f8b2e799ee913e3020bb3d2fe6aea Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 26 Aug 2025 17:31:17 +0200 Subject: [PATCH] [Serializer] Allow using attributes to declare compile-time serialization metadata --- UPGRADE-7.4.md | 1 + .../CacheWarmer/SerializerCacheWarmer.php | 7 +- .../FrameworkExtension.php | 37 +++++++++- .../FrameworkBundle/FrameworkBundle.php | 2 + .../Resources/config/serializer.php | 4 + ...serializer_mapping_without_annotations.php | 2 +- ...serializer_mapping_without_annotations.xml | 2 +- ...serializer_mapping_without_annotations.yml | 2 +- .../FrameworkExtensionTestCase.php | 12 +-- src/Symfony/Component/Serializer/CHANGELOG.md | 2 + .../AttributeMetadataPass.php | 48 ++++++++++++ .../Factory/ClassMetadataFactoryCompiler.php | 4 + .../Mapping/Loader/AttributeLoader.php | 21 +++++- .../Mapping/Loader/XmlFileLoader.php | 7 +- .../Mapping/Loader/YamlFileLoader.php | 7 +- .../AttributeMetadataPassTest.php | 74 +++++++++++++++++++ .../ClassMetadataFactoryCompilerTest.php | 4 + .../Mapping/Loader/AttributeLoaderTest.php | 25 +++++++ 18 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 src/Symfony/Component/Serializer/DependencyInjection/AttributeMetadataPass.php create mode 100644 src/Symfony/Component/Serializer/Tests/DependencyInjection/AttributeMetadataPassTest.php diff --git a/UPGRADE-7.4.md b/UPGRADE-7.4.md index 1686bc71c4c36..80d55d57f639d 100644 --- a/UPGRADE-7.4.md +++ b/UPGRADE-7.4.md @@ -95,6 +95,7 @@ Serializer * Make `AttributeMetadata` and `ClassMetadata` final * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters in attribute classes in favor of public properties + * Deprecate `ClassMetadataFactoryCompiler` String ------ diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php index fbf7083b70b28..ae6aa430b163c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -14,13 +14,14 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; /** - * Warms up XML and YAML serializer metadata. + * Warms up serializer metadata. * * @author Titouan Galopin * @@ -66,14 +67,14 @@ protected function doWarmUp(string $cacheDir, ArrayAdapter $arrayAdapter, ?strin /** * @param LoaderInterface[] $loaders * - * @return XmlFileLoader[]|YamlFileLoader[] + * @return list */ private function extractSupportedLoaders(array $loaders): array { $supportedLoaders = []; foreach ($loaders as $loader) { - if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) { + if (method_exists($loader, 'getMappedClasses')) { $supportedLoaders[] = $loader; } elseif ($loader instanceof LoaderChain) { $supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders())); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index a0a6a86ca24cb..54a6ae7c2b4b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -187,9 +187,10 @@ use Symfony\Component\Semaphore\Semaphore; use Symfony\Component\Semaphore\SemaphoreFactory; use Symfony\Component\Semaphore\Store\StoreFactory as SemaphoreStoreFactory; +use Symfony\Component\Serializer\Attribute as SerializerMapping; +use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; -use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; @@ -2073,10 +2074,38 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder } $serializerLoaders = []; - if ($config['enable_attributes'] ?? false) { - $attributeLoader = new Definition(AttributeLoader::class); - $serializerLoaders[] = $attributeLoader; + // When attributes are disabled, it means from runtime-discovery only; autoconfiguration should still happen. + // And when runtime-discovery of attributes is enabled, we can skip compile-time autoconfiguration in debug mode. + if (class_exists(SerializerAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug'))) { + // The $reflector argument hints at where the attribute could be used + $configurator = function (ChildDefinition $definition, object $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) { + $definition->addTag('serializer.attribute_metadata'); + }; + $container->registerAttributeForAutoconfiguration(SerializerMapping\Context::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\Groups::class, $configurator); + + $configurator = function (ChildDefinition $definition, object $attribute, \ReflectionMethod|\ReflectionProperty $reflector) { + $definition->addTag('serializer.attribute_metadata'); + }; + $container->registerAttributeForAutoconfiguration(SerializerMapping\Ignore::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\MaxDepth::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedName::class, $configurator); + $container->registerAttributeForAutoconfiguration(SerializerMapping\SerializedPath::class, $configurator); + + $container->registerAttributeForAutoconfiguration(SerializerMapping\DiscriminatorMap::class, function (ChildDefinition $definition) { + $definition->addTag('serializer.attribute_metadata'); + }); + } + + if (($config['enable_attributes'] ?? false) || class_exists(SerializerAttributeMetadataPass::class)) { + $serializerLoaders[] = new Reference('serializer.mapping.attribute_loader'); + + $container->getDefinition('serializer.mapping.attribute_loader') + ->replaceArgument(0, $config['enable_attributes'] ?? false); + } else { + // BC with symfony/serializer < 7.4 + $container->removeDefinition('serializer.mapping.attribute_services_loader'); } $fileRecorder = function ($extension, $path) use (&$serializerLoaders) { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 24ca5158a81a7..d8a6d8a4a7e58 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -65,6 +65,7 @@ use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass; use Symfony\Component\Runtime\SymfonyRuntime; use Symfony\Component\Scheduler\DependencyInjection\AddScheduleMessengerPass; +use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass as SerializerAttributeMetadataPass; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; use Symfony\Component\Translation\DependencyInjection\DataCollectorTranslatorPass; use Symfony\Component\Translation\DependencyInjection\LoggingTranslatorPass; @@ -170,6 +171,7 @@ public function build(ContainerBuilder $container): void $this->addCompilerPassIfExists($container, TranslationDumperPass::class); $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); + $this->addCompilerPassIfExists($container, SerializerAttributeMetadataPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); $this->addCompilerPassIfExists($container, PropertyInfoConstructorPass::class); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index e0a256bbe3640..6d9d354b4667b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -28,6 +28,7 @@ use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; @@ -151,6 +152,9 @@ ->set('serializer.mapping.chain_loader', LoaderChain::class) ->args([[]]) + ->set('serializer.mapping.attribute_loader', AttributeLoader::class) + ->args([true, []]) + // Class Metadata Factory ->set('serializer.mapping.class_metadata_factory', ClassMetadataFactory::class) ->args([service('serializer.mapping.chain_loader')]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php index 3e203028ce2ac..2ae08c77ac85b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/serializer_mapping_without_annotations.php @@ -6,7 +6,7 @@ 'handle_all_throwables' => true, 'php_errors' => ['log' => true], 'serializer' => [ - 'enable_attributes' => false, + 'enable_attributes' => true, 'mapping' => [ 'paths' => [ '%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml index bb8dccf9c3d62..165669fe6d1de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/serializer_mapping_without_annotations.xml @@ -7,7 +7,7 @@ - + %kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files %kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/serialization.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml index 46425dc942932..3c0e8be3b18f0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/serializer_mapping_without_annotations.yml @@ -5,7 +5,7 @@ framework: php_errors: log: true serializer: - enable_attributes: false + enable_attributes: true mapping: paths: - "%kernel.project_dir%/Fixtures/TestBundle/Resources/config/serializer_mapping/files" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 19af0678780a7..5af8f0c8d376c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -77,7 +77,6 @@ use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Security\Core\AuthenticationEvents; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; -use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; @@ -1571,7 +1570,7 @@ public function testSerializerEnabled() $argument = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0); $this->assertCount(2, $argument); - $this->assertEquals(AttributeLoader::class, $argument[0]->getClass()); + $this->assertEquals(new Reference('serializer.mapping.attribute_loader'), $argument[0]); $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)); } @@ -1761,6 +1760,7 @@ public function testSerializerMapping() $projectDir = $container->getParameter('kernel.project_dir'); $configDir = __DIR__.'/Fixtures/TestBundle/Resources/config'; $expectedLoaders = [ + new Reference('serializer.mapping.attribute_loader'), new Definition(XmlFileLoader::class, [$configDir.'/serialization.xml']), new Definition(YamlFileLoader::class, [$configDir.'/serialization.yml']), new Definition(YamlFileLoader::class, [$projectDir.'/config/serializer/foo.yml']), @@ -1770,15 +1770,15 @@ public function testSerializerMapping() new Definition(YamlFileLoader::class, [$configDir.'/serializer_mapping/serialization.yaml']), ]; - foreach ($expectedLoaders as $definition) { - if (is_file($arg = $definition->getArgument(0))) { - $definition->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); + foreach ($expectedLoaders as $loader) { + if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) { + $loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); } } $loaders = $container->getDefinition('serializer.mapping.chain_loader')->getArgument(0); foreach ($loaders as $loader) { - if (is_file($arg = $loader->getArgument(0))) { + if ($loader instanceof Definition && is_file($arg = $loader->getArgument(0))) { $loader->replaceArgument(0, strtr($arg, '/', \DIRECTORY_SEPARATOR)); } } diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index c44d6e605a291..4e17e8877916d 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -4,11 +4,13 @@ CHANGELOG 7.4 --- + * Add `AttributeMetadataPass` to declare compile-time constraint metadata using attributes * Add `CDATA_WRAPPING_NAME_PATTERN` support to `XmlEncoder` * Add support for `can*()` methods to `AttributeLoader` * Make `AttributeMetadata` and `ClassMetadata` final * Deprecate class aliases in the `Annotation` namespace, use attributes instead * Deprecate getters in attribute classes in favor of public properties + * Deprecate `ClassMetadataFactoryCompiler` 7.3 --- diff --git a/src/Symfony/Component/Serializer/DependencyInjection/AttributeMetadataPass.php b/src/Symfony/Component/Serializer/DependencyInjection/AttributeMetadataPass.php new file mode 100644 index 0000000000000..259c78edbacba --- /dev/null +++ b/src/Symfony/Component/Serializer/DependencyInjection/AttributeMetadataPass.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +final class AttributeMetadataPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('serializer.mapping.attribute_loader')) { + return; + } + + $resolve = $container->getParameterBag()->resolveValue(...); + $taggedClasses = []; + foreach ($container->getDefinitions() as $id => $definition) { + if (!$definition->hasTag('serializer.attribute_metadata')) { + continue; + } + if (!$definition->hasTag('container.excluded')) { + throw new InvalidArgumentException(\sprintf('The resource "%s" tagged "serializer.attribute_metadata" is missing the "container.excluded" tag.', $id)); + } + $taggedClasses[$resolve($definition->getClass())] = true; + } + + ksort($taggedClasses); + + if ($taggedClasses) { + $container->getDefinition('serializer.mapping.attribute_loader') + ->replaceArgument(1, array_keys($taggedClasses)); + } + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php index 942666801be7d..575019c54d8a9 100644 --- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassMetadataFactoryCompiler.php @@ -14,8 +14,12 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\VarExporter\VarExporter; +trigger_deprecation('symfony/serializer', '7.4', 'The "%s" class is deprecated.', ClassMetadataFactoryCompiler::class); + /** * @author Fabien Bourigault + * + * @deprecated since Symfony 7.4 */ final class ClassMetadataFactoryCompiler { diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php index 48aad1396a237..f590e6fbc9bef 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php @@ -43,12 +43,31 @@ class AttributeLoader implements LoaderInterface Context::class, ]; - public function __construct() + /** + * @param bool|null $allowAnyClass Null is allowed for BC with Symfony <= 6 + * @param class-string[] $mappedClasses + */ + public function __construct( + private ?bool $allowAnyClass = true, + private array $mappedClasses = [], + ) { + $this->allowAnyClass ??= true; + } + + /** + * @return class-string[] + */ + public function getMappedClasses(): array { + return $this->mappedClasses; } public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool { + if (!$this->allowAnyClass && !\in_array($classMetadata->getName(), $this->mappedClasses, true)) { + return false; + } + $reflectionClass = $classMetadata->getReflectionClass(); $className = $reflectionClass->name; $loaded = false; diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index ac6fee2db4177..fb64bb98c081e 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -29,7 +29,7 @@ class XmlFileLoader extends FileLoader /** * An array of {@class \SimpleXMLElement} instances. * - * @var \SimpleXMLElement[]|null + * @var array|null */ private ?array $classes = null; @@ -121,7 +121,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool /** * Return the names of the classes mapped in this file. * - * @return string[] + * @return class-string[] */ public function getMappedClasses(): array { @@ -144,6 +144,9 @@ private function parseFile(string $file): \SimpleXMLElement return simplexml_import_dom($dom); } + /** + * @return array + */ private function getClassesFromXml(): array { $xml = $this->parseFile($this->file); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 898ae9f1921b0..648de0db76c16 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -30,7 +30,7 @@ class YamlFileLoader extends FileLoader private ?Parser $yamlParser = null; /** - * An array of YAML class descriptions. + * @var array */ private ?array $classes = null; @@ -144,13 +144,16 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool /** * Return the names of the classes mapped in this file. * - * @return string[] + * @return class-string[] */ public function getMappedClasses(): array { return array_keys($this->classes ??= $this->getClassesFromYaml()); } + /** + * @return array + */ private function getClassesFromYaml(): array { if (!stream_is_local($this->file)) { diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/AttributeMetadataPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/AttributeMetadataPassTest.php new file mode 100644 index 0000000000000..0df41b6a7f6d6 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/AttributeMetadataPassTest.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Serializer\DependencyInjection\AttributeMetadataPass; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; + +class AttributeMetadataPassTest extends TestCase +{ + public function testProcessWithNoAttributeLoader() + { + $container = new ContainerBuilder(); + + // Should not throw any exception + (new AttributeMetadataPass())->process($container); + + $this->expectNotToPerformAssertions(); + } + + public function testProcessWithAttributeLoaderButNoTaggedServices() + { + $container = new ContainerBuilder(); + $container->register('serializer.mapping.attribute_loader', AttributeLoader::class) + ->setArguments([false, []]); + + // Should not throw any exception + (new AttributeMetadataPass())->process($container); + + $arguments = $container->getDefinition('serializer.mapping.attribute_loader')->getArguments(); + $this->assertSame([false, []], $arguments); + } + + public function testProcessWithTaggedServices() + { + $container = new ContainerBuilder(); + $container->setParameter('user_entity.class', 'App\Entity\User'); + + $container->register('serializer.mapping.attribute_loader', AttributeLoader::class) + ->setArguments([false, []]); + + $container->register('service1', '%user_entity.class%') + ->addTag('serializer.attribute_metadata') + ->addTag('container.excluded'); + $container->register('service2', 'App\Entity\Product') + ->addTag('serializer.attribute_metadata') + ->addTag('container.excluded'); + $container->register('service3', 'App\Entity\Order') + ->addTag('serializer.attribute_metadata') + ->addTag('container.excluded'); + // Classes should be deduplicated + $container->register('service4', 'App\Entity\Order') + ->addTag('serializer.attribute_metadata') + ->addTag('container.excluded'); + + (new AttributeMetadataPass())->process($container); + + $arguments = $container->getDefinition('serializer.mapping.attribute_loader')->getArguments(); + + // Classes should be sorted alphabetically + $expectedClasses = ['App\Entity\Order', 'App\Entity\Product', 'App\Entity\User']; + $this->assertSame([false, $expectedClasses], $arguments); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index aec9bcc916fba..9d45163deb21e 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Serializer\Tests\Mapping\Factory; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryCompiler; @@ -25,6 +27,8 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\SerializedPathInConstructorDummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; +#[IgnoreDeprecations] +#[Group('legacy')] final class ClassMetadataFactoryCompilerTest extends TestCase { private string $dumpPath; diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AttributeLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AttributeLoaderTest.php index b4550ba9f5326..8ad4d4e062b31 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AttributeLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AttributeLoaderTest.php @@ -247,6 +247,31 @@ public function testIgnoresAccessorishGetters() self::assertArrayNotHasKey('h', $attributesMetadata); } + public function testGetMappedClasses() + { + $mappedClasses = ['App\Entity\User', 'App\Entity\Product']; + $loader = new AttributeLoader(false, $mappedClasses); + + $this->assertSame($mappedClasses, $loader->getMappedClasses()); + } + + public function testLoadClassMetadataReturnsFalseForUnmappedClass() + { + $loader = new AttributeLoader(false, ['App\Entity\User']); + $classMetadata = new ClassMetadata('App\Entity\Product'); + + $this->assertFalse($loader->loadClassMetadata($classMetadata)); + } + + public function testLoadClassMetadataForMappedClassWithAttributes() + { + $loader = new AttributeLoader(false, [GroupDummy::class]); + $classMetadata = new ClassMetadata(GroupDummy::class); + + $this->assertTrue($loader->loadClassMetadata($classMetadata)); + $this->assertNotEmpty($classMetadata->getAttributesMetadata()); + } + protected function getLoaderForContextMapping(): AttributeLoader { return $this->loader;