diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 9f449d6a85..b4749b48d7 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -21,6 +21,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\JsonStreamerTransformerPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; @@ -59,5 +60,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new AuthenticatorManagerPass()); $container->addCompilerPass(new SerializerMappingLoaderPass()); $container->addCompilerPass(new MutatorPass()); + // Must run after Symfony's TransformerPass so we can rely on the value_object_transformer tag being processed. + $container->addCompilerPass(new JsonStreamerTransformerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10); } } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/JsonStreamerTransformerPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/JsonStreamerTransformerPass.php new file mode 100644 index 0000000000..0a48ca6e9c --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/JsonStreamerTransformerPass.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\JsonStreamer\Transformer\ValueObjectTransformerInterface; + +/** + * Builds a transformers locator merging "json_streamer.property_value_transformer", + * "json_streamer.value_transformer" (legacy) and "json_streamer.value_object_transformer" + * services, and assigns it to API Platform's custom JSON-LD stream reader/writer. + * + * FrameworkBundle's own TransformerPass only touches the standard json_streamer.stream_reader/writer + * services, not API Platform's JSON-LD-scoped ones; see https://github.com/symfony/symfony/pull/64190 + * for a proposed upstream fix that would make this pass obsolete. + * + * @internal + */ +final class JsonStreamerTransformerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!interface_exists(ValueObjectTransformerInterface::class)) { + return; + } + + if (!$container->hasDefinition('api_platform.jsonld.json_streamer.stream_reader') + && !$container->hasDefinition('api_platform.jsonld.json_streamer.stream_writer')) { + return; + } + + $map = []; + + foreach (['json_streamer.property_value_transformer', 'json_streamer.value_transformer'] as $tagName) { + foreach ($container->findTaggedServiceIds($tagName, true) as $id => $_) { + $map[$id] ??= new Reference($id); + } + } + + foreach ($container->findTaggedServiceIds('json_streamer.value_object_transformer', true) as $id => $_) { + $class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass()); + if (!\is_string($class) || !method_exists($class, 'getValueObjectClassName')) { + continue; + } + + $map[$class::getValueObjectClassName()] = new Reference($id); + } + + $argument = new ServiceLocatorArgument($map); + + foreach (['api_platform.jsonld.json_streamer.stream_reader', 'api_platform.jsonld.json_streamer.stream_writer'] as $serviceId) { + if ($container->hasDefinition($serviceId)) { + $container->getDefinition($serviceId)->replaceArgument(0, $argument); + } + } + } +} diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index f985c963cd..57f844672b 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\FilterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\JsonStreamerTransformerPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; @@ -57,6 +58,7 @@ public function testBuild(): void $containerProphecy->addCompilerPass(Argument::type(AuthenticatorManagerPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(SerializerMappingLoaderPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $containerProphecy->addCompilerPass(Argument::type(MutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(JsonStreamerTransformerPass::class), PassConfig::TYPE_BEFORE_OPTIMIZATION, -10)->willReturn($containerProphecy->reveal())->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal());