From b69759245487b01ce4a920204ff2699953ed5697 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 23 Jan 2026 15:27:19 +0100 Subject: [PATCH] fix(symfony): prevent symfony name converter service pollution --- .../MetadataAwareNameConverterPass.php | 23 ++++-- .../MetadataAwareNameConverterPassTest.php | 70 ++++++++++++------- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php index 4cb5ebc29c6..924ade8aa41 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPass.php @@ -16,11 +16,13 @@ use ApiPlatform\Metadata\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; /** - * Injects the metadata aware name converter if available. + * Creates API Platform's own metadata-aware name converter to avoid polluting Symfony's global serializer. * * @internal * @@ -36,17 +38,24 @@ final class MetadataAwareNameConverterPass implements CompilerPassInterface */ public function process(ContainerBuilder $container): void { - if (!$container->hasDefinition('serializer.name_converter.metadata_aware')) { + if (!$container->hasDefinition('serializer.mapping.class_metadata_factory')) { return; } - if ($container->hasAlias('api_platform.name_converter')) { - $nameConverter = (string) $container->getAlias('api_platform.name_converter'); + $fallbackConverter = null; - $container->setParameter('.serializer.name_converter', $nameConverter); - $container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($nameConverter)); + // Check if user explicitly configured a name converter for API Platform + if ($container->hasAlias('api_platform.name_converter')) { + $fallbackConverter = new Reference((string) $container->getAlias('api_platform.name_converter')); } - $container->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware'); + // Create API Platform's own metadata-aware name converter (isolated from Symfony's) + $definition = new Definition(MetadataAwareNameConverter::class, [ + new Reference('serializer.mapping.class_metadata_factory'), + $fallbackConverter, + ]); + + $container->setDefinition('api_platform.name_converter.metadata_aware', $definition); + $container->setAlias('api_platform.name_converter', 'api_platform.name_converter.metadata_aware'); } } diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php index bb8f1df8b61..db5165706de 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/MetadataAwareNameConverterPassTest.php @@ -15,20 +15,18 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; /** * @author Antoine Bluchet */ class MetadataAwareNameConverterPassTest extends TestCase { - use ProphecyTrait; - public function testConstruct(): void { $this->assertInstanceOf(CompilerPassInterface::class, new MetadataAwareNameConverterPass()); @@ -38,42 +36,66 @@ public function testProcessWithNameConverter(): void { $pass = new MetadataAwareNameConverterPass(); - $reference = new Reference('app.name_converter'); + $container = new ContainerBuilder(); + $container->setDefinition('serializer.mapping.class_metadata_factory', new Definition()); + $container->setAlias('api_platform.name_converter', 'app.name_converter'); + + $pass->process($container); - $definition = $this->prophesize(Definition::class); - $definition->setArgument(1, $reference)->shouldBeCalled()->willReturn($definition); + // Should create API Platform's own metadata-aware converter + $this->assertTrue($container->hasDefinition('api_platform.name_converter.metadata_aware')); + $definition = $container->getDefinition('api_platform.name_converter.metadata_aware'); + $this->assertSame(MetadataAwareNameConverter::class, $definition->getClass()); - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->hasAlias('api_platform.name_converter')->willReturn(true)->shouldBeCalled(); - $containerBuilderProphecy->getAlias('api_platform.name_converter')->shouldBeCalled()->willReturn(new Alias('app.name_converter')); - $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn(true); - $containerBuilderProphecy->getDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn($definition); - $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldBeCalled(); - $containerBuilderProphecy->setParameter('.serializer.name_converter', 'app.name_converter')->shouldBeCalled(); + // Should have class metadata factory as first argument + $args = $definition->getArguments(); + $this->assertInstanceOf(Reference::class, $args[0]); + $this->assertSame('serializer.mapping.class_metadata_factory', (string) $args[0]); - $pass->process($containerBuilderProphecy->reveal()); + // Should have the user's converter as fallback (second argument) + $this->assertInstanceOf(Reference::class, $args[1]); + $this->assertSame('app.name_converter', (string) $args[1]); + + // Should alias api_platform.name_converter to the new service + $this->assertTrue($container->hasAlias('api_platform.name_converter')); + $alias = $container->getAlias('api_platform.name_converter'); + $this->assertSame('api_platform.name_converter.metadata_aware', (string) $alias); } public function testProcessWithoutNameConverter(): void { $pass = new MetadataAwareNameConverterPass(); - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->hasAlias('api_platform.name_converter')->willReturn(false)->shouldBeCalled(); - $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->shouldBeCalled()->willReturn(true); - $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldBeCalled(); + $container = new ContainerBuilder(); + $container->setDefinition('serializer.mapping.class_metadata_factory', new Definition()); + + $pass->process($container); - $pass->process($containerBuilderProphecy->reveal()); + // Should still create API Platform's own metadata-aware converter (without fallback) + $this->assertTrue($container->hasDefinition('api_platform.name_converter.metadata_aware')); + $definition = $container->getDefinition('api_platform.name_converter.metadata_aware'); + $this->assertSame(MetadataAwareNameConverter::class, $definition->getClass()); + + $args = $definition->getArguments(); + $this->assertInstanceOf(Reference::class, $args[0]); + $this->assertSame('serializer.mapping.class_metadata_factory', (string) $args[0]); + $this->assertNull($args[1]); // No fallback converter + + // Should alias api_platform.name_converter to the new service + $this->assertTrue($container->hasAlias('api_platform.name_converter')); } - public function testProcessWithoutMetadataAwareDefinition(): void + public function testProcessWithoutClassMetadataFactory(): void { $pass = new MetadataAwareNameConverterPass(); - $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); - $containerBuilderProphecy->hasDefinition('serializer.name_converter.metadata_aware')->willReturn(false)->shouldBeCalled(); - $containerBuilderProphecy->setAlias('api_platform.name_converter', 'serializer.name_converter.metadata_aware')->shouldNotBeCalled(); + $container = new ContainerBuilder(); + // No serializer.mapping.class_metadata_factory defined + + $pass->process($container); - $pass->process($containerBuilderProphecy->reveal()); + // Should not create anything if class metadata factory is missing + $this->assertFalse($container->hasDefinition('api_platform.name_converter.metadata_aware')); + $this->assertFalse($container->hasAlias('api_platform.name_converter')); } }