From b20009c9c2de51740353696b3a4595a02c274cdd Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 31 Mar 2025 03:12:08 +0200 Subject: [PATCH] [Serializer][FrameworkBundle] Allow setting `$objectClassResolver` via configuration --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 9 +++ .../FrameworkExtension.php | 4 ++ src/Symfony/Component/Serializer/CHANGELOG.md | 1 + .../DependencyInjection/SerializerPass.php | 26 ++++++++ .../Normalizer/AbstractObjectNormalizer.php | 2 +- .../SerializerPassTest.php | 65 +++++++++++++++++-- 7 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 9975642622b13..56cbbda9c3f75 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -21,6 +21,7 @@ CHANGELOG * Allow configuring the logging channel per type of exceptions * Enable service argument resolution on classes that use the `#[Route]` attribute, the `#[AsController]` attribute is no longer required + * Add `framework.serializer.object_class_resolver` option 7.2 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index aa61cb12c56f4..cfd6d1a1d9dc8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1172,6 +1172,13 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->defaultValue([]) ->prototype('variable')->end() ; + $objectClassResolver = fn () => (new NodeBuilder()) + ->variableNode('object_class_resolver') + ->validate() + ->ifTrue(fn ($v) => !\is_string($v) && !\is_callable($v)) + ->thenInvalid('The "object_class_resolver" parameter must be a string or a callable.') + ->end() + ; $rootNode ->children() @@ -1182,6 +1189,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->children() ->booleanNode('enable_attributes')->{class_exists(FullStack::class) ? 'defaultFalse' : 'defaultTrue'}()->end() ->scalarNode('name_converter')->end() + ->append($objectClassResolver()) ->scalarNode('circular_reference_handler')->end() ->scalarNode('max_depth_handler')->end() ->arrayNode('mapping') @@ -1199,6 +1207,7 @@ private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $e ->arrayPrototype() ->children() ->scalarNode('name_converter')->end() + ->append($objectClassResolver()) ->append($defaultContextNode()) ->booleanNode('include_built_in_normalizers') ->info('Whether to include the built-in normalizers') diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7e500af886941..cc7c9efbabee7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2070,6 +2070,10 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->setParameter('serializer.default_context', $defaultContext); } + if ($config['object_class_resolver'] ?? false) { + $container->setParameter('.serializer.object_class_resolver', $config['object_class_resolver']); + } + if ($config['circular_reference_handler'] ?? false) { $container->setParameter('.serializer.circular_reference_handler', $config['circular_reference_handler']); } diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 1b5c95cd39443..42532b89a2d6d 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Register `NormalizerInterface` and `DenormalizerInterface` aliases for named serializers * Add `NumberNormalizer` to normalize `BcMath\Number` and `GMP` as `string` * Add `defaultType` to `DiscriminatorMap` + * Support setting `$objectClassResolver` via `SerializerPass` 7.2 --- diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php index 994f61246a342..5ef5dde623e24 100644 --- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php +++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -74,6 +75,10 @@ public function process(ContainerBuilder $container): void $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $defaultContext, $circularReferenceHandler, $maxDepthHandler); + if ($container->hasParameter('.serializer.object_class_resolver')) { + $this->setObjectClassResolver($container, $normalizers, $container->getParameter('.serializer.object_class_resolver')); + } + $this->configureSerializer($container, 'serializer', $normalizers, $encoders, 'default'); if ($namedSerializers) { @@ -130,6 +135,23 @@ private function bindDefaultContext(ContainerBuilder $container, array $services } } + private function setObjectClassResolver(ContainerBuilder $container, array $services, string|callable $objectClassResolver): void + { + foreach ($services as $id) { + $definition = $container->getDefinition((string) $id); + + if (!is_a($definition->getClass(), AbstractObjectNormalizer::class, true)) { + continue; + } + + if (!\is_callable($objectClassResolver)) { + $objectClassResolver = new Reference($objectClassResolver); + } + + $definition->setArgument('$objectClassResolver', $objectClassResolver); + } + } + private function configureSerializer(ContainerBuilder $container, string $id, array $normalizers, array $encoders, string $serializerName): void { if ($container->getParameter('kernel.debug') && $container->hasDefinition('serializer.data_collector')) { @@ -175,6 +197,10 @@ private function configureNamedSerializers(ContainerBuilder $container, ?string $this->bindDefaultContext($container, array_merge($normalizers, $encoders), $config['default_context'], $circularReferenceHandler, $maxDepthHandler); + if ($config['object_class_resolver'] ?? false) { + $this->setObjectClassResolver($container, $normalizers, $config['object_class_resolver']); + } + $container->registerChild($serializerId, 'serializer')->setArgument('$defaultContext', $config['default_context']); $container->registerAliasForArgument($serializerId, SerializerInterface::class, $serializerName.'.serializer'); $container->registerAliasForArgument($serializerId, NormalizerInterface::class, $serializerName.'.normalizer'); diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index c346aafa8f450..66f21e439976d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -1052,7 +1052,7 @@ private function updateData(array $data, string $attribute, mixed $attributeValu */ private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool { - if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false) + if (!($context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false) || !isset($attributesMetadata[$attribute]) || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth() ) { return false; diff --git a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php index dc8d0c757185d..4319f8cd48806 100644 --- a/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php +++ b/src/Symfony/Component/Serializer/Tests/DependencyInjection/SerializerPassTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Debug\TraceableNormalizer; use Symfony\Component\Serializer\Debug\TraceableSerializer; use Symfony\Component\Serializer\DependencyInjection\SerializerPass; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -115,8 +116,7 @@ public function testBindObjectNormalizerDefaultContext(array $parameters, array $container->setParameter('kernel.debug', false); $container->register('serializer')->setArguments([null, null, []]); $container->getParameterBag()->add($parameters); - $definition = $container->register('serializer.normalizer.object') - ->setClass(ObjectNormalizer::class) + $definition = $container->register('serializer.normalizer.object', ObjectNormalizer::class) ->addTag('serializer.normalizer') ->addTag('serializer.encoder') ; @@ -128,6 +128,40 @@ public function testBindObjectNormalizerDefaultContext(array $parameters, array $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); } + /** + * @dataProvider provideSetObjectClassResolverData + */ + public function testSetObjectClassResolver(array $parameters, array $expectedArguments) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->register('serializer')->setArguments([null, null]); + $container->getParameterBag()->add($parameters); + $definition = $container->register('n1', AbstractObjectNormalizer::class)->addTag('serializer.normalizer')->addTag('serializer.encoder'); + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $this->assertEquals($expectedArguments, $definition->getArguments()); + } + + public static function provideSetObjectClassResolverData(): iterable + { + yield [[], []]; + yield [ + ['.serializer.object_class_resolver' => 'strlen'], + ['$objectClassResolver' => 'strlen'], + ]; + yield [ + ['.serializer.object_class_resolver' => [self::class, 'provideSetObjectClassResolverData']], + ['$objectClassResolver' => [self::class, 'provideSetObjectClassResolverData']], + ]; + yield [ + ['.serializer.object_class_resolver' => 'my.service'], + ['$objectClassResolver' => new Reference('my.service')], + ]; + } + public function testNormalizersAndEncodersAreDecoratedAndOrderedWhenCollectingData() { $container = new ContainerBuilder(); @@ -622,8 +656,7 @@ public function testBindNamedSerializerObjectNormalizerDefaultContext(array $def $container->register('serializer')->setArguments([null, null, []]); $container->getParameterBag()->add($parameters); - $container->register('serializer.normalizer.object') - ->setClass(ObjectNormalizer::class) + $container->register('serializer.normalizer.object', ObjectNormalizer::class) ->addTag('serializer.normalizer', ['serializer' => '*']) ->addTag('serializer.encoder', ['serializer' => '*']) ; @@ -636,6 +669,30 @@ public function testBindNamedSerializerObjectNormalizerDefaultContext(array $def $this->assertEquals($bindings['array $defaultContext'], new BoundArgument($context, false)); } + /** + * @dataProvider provideSetObjectClassResolverData + */ + public function testSetObjectClassResolverToNamedSerializers(array $parameters, array $expectedArguments) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', false); + $container->setParameter('.serializer.named_serializers', [ + 'api' => ['object_class_resolver' => $parameters['.serializer.object_class_resolver'] ?? null], + ]); + + $container->register('serializer')->setArguments([null, null]); + $container->register('n1', AbstractObjectNormalizer::class) + ->addTag('serializer.normalizer', ['serializer' => '*']) + ->addTag('serializer.encoder', ['serializer' => '*']) + ; + + $serializerPass = new SerializerPass(); + $serializerPass->process($container); + + $definition = $container->getDefinition('n1.api'); + $this->assertEquals($expectedArguments, $definition->getArguments()); + } + public function testNamedSerializersAreRegistered() { $container = new ContainerBuilder();