From 3851c12f8bf4ba3c169634aa7634ede086034917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Mon, 11 May 2026 22:17:31 +0200 Subject: [PATCH 1/3] fix(symfony): register property_info fallback when not provided by Symfony MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When symfony/framework-bundle has property_info disabled (the default on full-stack Symfony unless explicitly opted-in), the prependExtensionConfig fallback in ApiPlatformExtension::prepend() can be overridden by user configuration, resulting in "service not found" errors at container compile time. Add a PropertyInfoPass compiler pass that registers a minimal PropertyInfoExtractor backed by ReflectionExtractor when the property_info service is absent, ensuring API Platform never requires it to be explicitly enabled. Fixes #7876 Signed-off-by: Guillaume Delré --- src/Symfony/Bundle/ApiPlatformBundle.php | 2 + .../Compiler/PropertyInfoPass.php | 61 +++++++++++++++ .../Compiler/PropertyInfoPassTest.php | 77 +++++++++++++++++++ .../Symfony/Bundle/ApiPlatformBundleTest.php | 46 +++++------ 4 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php create mode 100644 src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index b4749b48d7c..26c6d87ba69 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -24,6 +24,7 @@ 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\PropertyInfoPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -58,6 +59,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestClientPass()); $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); + $container->addCompilerPass(new PropertyInfoPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); $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. diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php new file mode 100644 index 00000000000..96cec55d801 --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php @@ -0,0 +1,61 @@ + + * + * 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\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + +/** + * Ensures the property_info service is always available. + * + * When symfony/framework-bundle has property_info disabled (which is the default + * on a full-stack Symfony application unless explicitly opted-in), API Platform's + * prependExtensionConfig() fallback can be overridden by user configuration. + * This pass registers a minimal fallback so API Platform never fails with + * "service not found" for property_info. + * + * @internal + */ +final class PropertyInfoPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if ($container->hasDefinition('property_info') || $container->hasAlias('property_info')) { + return; + } + + if (!$container->hasDefinition('property_info.reflection_extractor')) { + $reflectionExtractor = new Definition(ReflectionExtractor::class); + $reflectionExtractor->addTag('property_info.list_extractor', ['priority' => -1000]); + $reflectionExtractor->addTag('property_info.type_extractor', ['priority' => -1002]); + $reflectionExtractor->addTag('property_info.access_extractor', ['priority' => -1000]); + $reflectionExtractor->addTag('property_info.initializable_extractor', ['priority' => -1000]); + $container->setDefinition('property_info.reflection_extractor', $reflectionExtractor); + } + + $definition = new Definition(PropertyInfoExtractor::class); + $definition->setArguments([ + new TaggedIteratorArgument('property_info.list_extractor'), + new TaggedIteratorArgument('property_info.type_extractor'), + new TaggedIteratorArgument('property_info.description_extractor'), + new TaggedIteratorArgument('property_info.access_extractor'), + new TaggedIteratorArgument('property_info.initializable_extractor'), + ]); + $container->setDefinition('property_info', $definition); + } +} diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php new file mode 100644 index 00000000000..1ce0b1982f8 --- /dev/null +++ b/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php @@ -0,0 +1,77 @@ + + * + * 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\Tests\Bundle\DependencyInjection\Compiler; + +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\PropertyInfoPass; +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + +class PropertyInfoPassTest extends TestCase +{ + public function testRegistersPropertyInfoFallbackWhenMissing(): void + { + $container = new ContainerBuilder(); + + (new PropertyInfoPass())->process($container); + + $this->assertTrue($container->hasDefinition('property_info')); + $this->assertTrue($container->hasDefinition('property_info.reflection_extractor')); + + $definition = $container->getDefinition('property_info'); + $this->assertSame(PropertyInfoExtractor::class, $definition->getClass()); + + $reflectionDef = $container->getDefinition('property_info.reflection_extractor'); + $this->assertSame(ReflectionExtractor::class, $reflectionDef->getClass()); + $this->assertArrayHasKey('property_info.list_extractor', $reflectionDef->getTags()); + $this->assertArrayHasKey('property_info.type_extractor', $reflectionDef->getTags()); + $this->assertArrayHasKey('property_info.access_extractor', $reflectionDef->getTags()); + $this->assertArrayHasKey('property_info.initializable_extractor', $reflectionDef->getTags()); + } + + public function testSkipsWhenPropertyInfoDefinitionExists(): void + { + $container = new ContainerBuilder(); + $container->register('property_info', PropertyInfoExtractor::class); + + (new PropertyInfoPass())->process($container); + + $this->assertFalse($container->hasDefinition('property_info.reflection_extractor')); + } + + public function testSkipsWhenPropertyInfoAliasExists(): void + { + $container = new ContainerBuilder(); + $container->register('some_property_info', PropertyInfoExtractor::class); + $container->setAlias('property_info', 'some_property_info'); + + (new PropertyInfoPass())->process($container); + + $this->assertFalse($container->hasDefinition('property_info.reflection_extractor')); + } + + public function testDoesNotRegisterReflectionExtractorIfAlreadyPresent(): void + { + $container = new ContainerBuilder(); + $container->register('property_info.reflection_extractor', ReflectionExtractor::class); + + (new PropertyInfoPass())->process($container); + + $this->assertTrue($container->hasDefinition('property_info')); + $existingDef = $container->getDefinition('property_info.reflection_extractor'); + $this->assertSame(ReflectionExtractor::class, $existingDef->getClass()); + $this->assertEmpty($existingDef->getTags()); + } +} diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 57f844672b2..083a5ac94a8 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -25,13 +25,11 @@ 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\PropertyInfoPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; /** @@ -39,28 +37,30 @@ */ class ApiPlatformBundleTest extends TestCase { - use ProphecyTrait; - public function testBuild(): void { - $containerProphecy = $this->prophesize(ContainerBuilder::class); - // TODO: remove in 5.x - $containerProphecy->addCompilerPass(Argument::type(DataProviderPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(AttributeFilterPass::class), PassConfig::TYPE_BEFORE_OPTIMIZATION, 101)->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(AttributeResourcePass::class))->shouldBeCalled()->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(FilterPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(ElasticsearchClientPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(GraphQlTypePass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(GraphQlResolverPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(MetadataAwareNameConverterPass::class), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100)->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(TestClientPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(TestMercureHubPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $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(); - + $container = new ContainerBuilder(); $bundle = new ApiPlatformBundle(); - $bundle->build($containerProphecy->reveal()); + $bundle->build($container); + + $passes = $container->getCompilerPassConfig()->getBeforeOptimizationPasses(); + $passClasses = array_map(static fn (object $p): string => $p::class, $passes); + + // TODO: remove in 5.x + $this->assertContains(DataProviderPass::class, $passClasses); + $this->assertContains(AttributeFilterPass::class, $passClasses); + $this->assertContains(AttributeResourcePass::class, $passClasses); + $this->assertContains(FilterPass::class, $passClasses); + $this->assertContains(ElasticsearchClientPass::class, $passClasses); + $this->assertContains(GraphQlTypePass::class, $passClasses); + $this->assertContains(GraphQlResolverPass::class, $passClasses); + $this->assertContains(MetadataAwareNameConverterPass::class, $passClasses); + $this->assertContains(TestClientPass::class, $passClasses); + $this->assertContains(TestMercureHubPass::class, $passClasses); + $this->assertContains(AuthenticatorManagerPass::class, $passClasses); + $this->assertContains(PropertyInfoPass::class, $passClasses); + $this->assertContains(SerializerMappingLoaderPass::class, $passClasses); + $this->assertContains(MutatorPass::class, $passClasses); + $this->assertContains(JsonStreamerTransformerPass::class, $passClasses); } } From 6150a1f0c53fa4974292c8032229bceaf9708433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillaume=20Delr=C3=A9?= Date: Wed, 13 May 2026 13:59:33 +0200 Subject: [PATCH 2/3] fix(symfony): decouple api_platform.property_info from framework service Replace the alias `api_platform.property_info -> property_info` with a self-contained PropertyInfoExtractor backed by tagged_iterator, so the service no longer depends on the framework's property_info being registered. Simplify PropertyInfoPass: instead of squatting on the framework's `property_info` service ID, only register `api_platform.property_info.reflection_extractor` with the standard `property_info.*` tags as a baseline when FrameworkBundle's own extractor is absent. --- .../Compiler/PropertyInfoPass.php | 37 +++++-------------- src/Symfony/Bundle/Resources/config/api.php | 10 ++++- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php index 96cec55d801..08c06eb7ad2 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php @@ -13,21 +13,14 @@ namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler; -use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; -use Symfony\Component\PropertyInfo\PropertyInfoExtractor; /** - * Ensures the property_info service is always available. - * - * When symfony/framework-bundle has property_info disabled (which is the default - * on a full-stack Symfony application unless explicitly opted-in), API Platform's - * prependExtensionConfig() fallback can be overridden by user configuration. - * This pass registers a minimal fallback so API Platform never fails with - * "service not found" for property_info. + * Registers a ReflectionExtractor fallback for api_platform.property_info when + * framework.property_info is disabled, so tagged_iterator('property_info.*') is never empty. * * @internal */ @@ -35,27 +28,15 @@ final class PropertyInfoPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - if ($container->hasDefinition('property_info') || $container->hasAlias('property_info')) { + if ($container->hasDefinition('property_info.reflection_extractor')) { return; } - if (!$container->hasDefinition('property_info.reflection_extractor')) { - $reflectionExtractor = new Definition(ReflectionExtractor::class); - $reflectionExtractor->addTag('property_info.list_extractor', ['priority' => -1000]); - $reflectionExtractor->addTag('property_info.type_extractor', ['priority' => -1002]); - $reflectionExtractor->addTag('property_info.access_extractor', ['priority' => -1000]); - $reflectionExtractor->addTag('property_info.initializable_extractor', ['priority' => -1000]); - $container->setDefinition('property_info.reflection_extractor', $reflectionExtractor); - } - - $definition = new Definition(PropertyInfoExtractor::class); - $definition->setArguments([ - new TaggedIteratorArgument('property_info.list_extractor'), - new TaggedIteratorArgument('property_info.type_extractor'), - new TaggedIteratorArgument('property_info.description_extractor'), - new TaggedIteratorArgument('property_info.access_extractor'), - new TaggedIteratorArgument('property_info.initializable_extractor'), - ]); - $container->setDefinition('property_info', $definition); + $definition = new Definition(ReflectionExtractor::class); + $definition->addTag('property_info.list_extractor', ['priority' => -1000]); + $definition->addTag('property_info.type_extractor', ['priority' => -1002]); + $definition->addTag('property_info.access_extractor', ['priority' => -1000]); + $definition->addTag('property_info.initializable_extractor', ['priority' => -1000]); + $container->setDefinition('api_platform.property_info.reflection_extractor', $definition); } } diff --git a/src/Symfony/Bundle/Resources/config/api.php b/src/Symfony/Bundle/Resources/config/api.php index c1685a0c4ae..39f4a9704f3 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -46,6 +46,7 @@ use ApiPlatform\Symfony\Routing\Router; use ApiPlatform\Symfony\Routing\SkolemIriConverter; use Negotiation\Negotiator; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -68,7 +69,14 @@ $services->alias('api_platform.property_accessor', 'property_accessor'); - $services->alias('api_platform.property_info', 'property_info'); + $services->set('api_platform.property_info', PropertyInfoExtractor::class) + ->args([ + tagged_iterator('property_info.list_extractor'), + tagged_iterator('property_info.type_extractor'), + tagged_iterator('property_info.description_extractor'), + tagged_iterator('property_info.access_extractor'), + tagged_iterator('property_info.initializable_extractor'), + ]); $services->set('api_platform.negotiator', Negotiator::class); From 0d507e7e812ec5dd3ef2f98497816f5453aee9ca Mon Sep 17 00:00:00 2001 From: soyuka Date: Sat, 16 May 2026 08:59:16 +0200 Subject: [PATCH 3/3] refactor(symfony): self-contained property_info chain with cache decoration | Q | A | ------------- | --- | Branch? | main | Tickets | Fix #7876 | License | MIT | Doc PR | Closes api-platform/docs#2262 Drop the PropertyInfoPass compiler pass and register a self-sufficient extractor chain directly in api.php: ReflectionExtractor always, plus PhpDocExtractor and PhpStanExtractor under class_exists guards. Decorate api_platform.property_info with PropertyInfoCacheExtractor over the cache.property_info pool so metadata extraction is cached regardless of framework.property_info being enabled. Extractors remain tagged on property_info.* so ecosystem extractors (e.g. Doctrine's) still flow into the composite via tagged_iterator. --- src/Symfony/Bundle/ApiPlatformBundle.php | 2 - .../Compiler/PropertyInfoPass.php | 42 ---------- src/Symfony/Bundle/Resources/config/api.php | 37 +++++++++ .../Compiler/PropertyInfoPassTest.php | 77 ------------------- .../Symfony/Bundle/ApiPlatformBundleTest.php | 2 - 5 files changed, 37 insertions(+), 123 deletions(-) delete mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php delete mode 100644 src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 26c6d87ba69..b4749b48d7c 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -24,7 +24,6 @@ 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\PropertyInfoPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -59,7 +58,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestClientPass()); $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); - $container->addCompilerPass(new PropertyInfoPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); $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. diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php deleted file mode 100644 index 08c06eb7ad2..00000000000 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * 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\Compiler\CompilerPassInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; - -/** - * Registers a ReflectionExtractor fallback for api_platform.property_info when - * framework.property_info is disabled, so tagged_iterator('property_info.*') is never empty. - * - * @internal - */ -final class PropertyInfoPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container): void - { - if ($container->hasDefinition('property_info.reflection_extractor')) { - return; - } - - $definition = new Definition(ReflectionExtractor::class); - $definition->addTag('property_info.list_extractor', ['priority' => -1000]); - $definition->addTag('property_info.type_extractor', ['priority' => -1002]); - $definition->addTag('property_info.access_extractor', ['priority' => -1000]); - $definition->addTag('property_info.initializable_extractor', ['priority' => -1000]); - $container->setDefinition('api_platform.property_info.reflection_extractor', $definition); - } -} diff --git a/src/Symfony/Bundle/Resources/config/api.php b/src/Symfony/Bundle/Resources/config/api.php index 39f4a9704f3..32fa4e16f86 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -46,6 +46,13 @@ use ApiPlatform\Symfony\Routing\Router; use ApiPlatform\Symfony\Routing\SkolemIriConverter; use Negotiation\Negotiator; +use phpDocumentor\Reflection\DocBlockFactory; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use Symfony\Component\PropertyInfo\Extractor\ConstructorExtractor; +use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -69,6 +76,29 @@ $services->alias('api_platform.property_accessor', 'property_accessor'); + $services->set('api_platform.property_info.reflection_extractor', ReflectionExtractor::class) + ->tag('property_info.list_extractor', ['priority' => -1000]) + ->tag('property_info.type_extractor', ['priority' => -1002]) + ->tag('property_info.access_extractor', ['priority' => -1000]) + ->tag('property_info.initializable_extractor', ['priority' => -1000]); + + if (class_exists(DocBlockFactory::class)) { + $services->set('api_platform.property_info.php_doc_extractor', PhpDocExtractor::class) + ->tag('property_info.description_extractor', ['priority' => -1000]) + ->tag('property_info.type_extractor', ['priority' => -1001]) + ->tag('property_info.constructor_extractor', ['priority' => -1001]); + } + + if (class_exists(PhpDocParser::class)) { + $services->set('api_platform.property_info.phpstan_extractor', PhpStanExtractor::class) + ->tag('property_info.type_extractor', ['priority' => -1000]) + ->tag('property_info.constructor_extractor', ['priority' => -1000]); + } + + $services->set('api_platform.property_info.constructor_extractor', ConstructorExtractor::class) + ->args([tagged_iterator('property_info.constructor_extractor')]) + ->tag('property_info.type_extractor', ['priority' => -999]); + $services->set('api_platform.property_info', PropertyInfoExtractor::class) ->args([ tagged_iterator('property_info.list_extractor'), @@ -78,6 +108,13 @@ tagged_iterator('property_info.initializable_extractor'), ]); + $services->set('api_platform.property_info.cache', PropertyInfoCacheExtractor::class) + ->decorate('api_platform.property_info') + ->args([ + service('.inner'), + service('cache.property_info'), + ]); + $services->set('api_platform.negotiator', Negotiator::class); $services->set('api_platform.resource_class_resolver', ResourceClassResolver::class) diff --git a/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php b/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php deleted file mode 100644 index 1ce0b1982f8..00000000000 --- a/src/Symfony/Tests/Bundle/DependencyInjection/Compiler/PropertyInfoPassTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * 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\Tests\Bundle\DependencyInjection\Compiler; - -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\PropertyInfoPass; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; -use Symfony\Component\PropertyInfo\PropertyInfoExtractor; - -class PropertyInfoPassTest extends TestCase -{ - public function testRegistersPropertyInfoFallbackWhenMissing(): void - { - $container = new ContainerBuilder(); - - (new PropertyInfoPass())->process($container); - - $this->assertTrue($container->hasDefinition('property_info')); - $this->assertTrue($container->hasDefinition('property_info.reflection_extractor')); - - $definition = $container->getDefinition('property_info'); - $this->assertSame(PropertyInfoExtractor::class, $definition->getClass()); - - $reflectionDef = $container->getDefinition('property_info.reflection_extractor'); - $this->assertSame(ReflectionExtractor::class, $reflectionDef->getClass()); - $this->assertArrayHasKey('property_info.list_extractor', $reflectionDef->getTags()); - $this->assertArrayHasKey('property_info.type_extractor', $reflectionDef->getTags()); - $this->assertArrayHasKey('property_info.access_extractor', $reflectionDef->getTags()); - $this->assertArrayHasKey('property_info.initializable_extractor', $reflectionDef->getTags()); - } - - public function testSkipsWhenPropertyInfoDefinitionExists(): void - { - $container = new ContainerBuilder(); - $container->register('property_info', PropertyInfoExtractor::class); - - (new PropertyInfoPass())->process($container); - - $this->assertFalse($container->hasDefinition('property_info.reflection_extractor')); - } - - public function testSkipsWhenPropertyInfoAliasExists(): void - { - $container = new ContainerBuilder(); - $container->register('some_property_info', PropertyInfoExtractor::class); - $container->setAlias('property_info', 'some_property_info'); - - (new PropertyInfoPass())->process($container); - - $this->assertFalse($container->hasDefinition('property_info.reflection_extractor')); - } - - public function testDoesNotRegisterReflectionExtractorIfAlreadyPresent(): void - { - $container = new ContainerBuilder(); - $container->register('property_info.reflection_extractor', ReflectionExtractor::class); - - (new PropertyInfoPass())->process($container); - - $this->assertTrue($container->hasDefinition('property_info')); - $existingDef = $container->getDefinition('property_info.reflection_extractor'); - $this->assertSame(ReflectionExtractor::class, $existingDef->getClass()); - $this->assertEmpty($existingDef->getTags()); - } -} diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 083a5ac94a8..660f12243d3 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -25,7 +25,6 @@ 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\PropertyInfoPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -58,7 +57,6 @@ public function testBuild(): void $this->assertContains(TestClientPass::class, $passClasses); $this->assertContains(TestMercureHubPass::class, $passClasses); $this->assertContains(AuthenticatorManagerPass::class, $passClasses); - $this->assertContains(PropertyInfoPass::class, $passClasses); $this->assertContains(SerializerMappingLoaderPass::class, $passClasses); $this->assertContains(MutatorPass::class, $passClasses); $this->assertContains(JsonStreamerTransformerPass::class, $passClasses);