From 852cb799b32b4f8e4c4fb503275c21fa0081e09e Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 11 May 2026 18:35:21 +0200 Subject: [PATCH] fix(symfony): include value-object transformers in JSON-LD streamer locator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symfony 8.1 split json_streamer.value_transformer into property_value_transformer and value_object_transformer. The custom JSON-LD JsonStreamReader/Writer bypassed Symfony's TransformerPass, so DateTimeInterface (and other value objects) were missing from its locator — making the generator emit a Splitter::splitDict call on date-time strings ("JSON is not valid"). --- src/Symfony/Bundle/ApiPlatformBundle.php | 3 + .../Compiler/JsonStreamerTransformerPass.php | 71 +++++++++++++++++++ .../Symfony/Bundle/ApiPlatformBundleTest.php | 2 + 3 files changed, 76 insertions(+) create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/JsonStreamerTransformerPass.php diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 9f449d6a854..b4749b48d7c 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 00000000000..0a48ca6e9c1 --- /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 f985c963cdf..57f844672b2 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());