diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 9f449d6a854..6488c6809c4 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -23,6 +23,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; 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; @@ -57,6 +58,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()); } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php new file mode 100644 index 00000000000..08c06eb7ad2 --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/PropertyInfoPass.php @@ -0,0 +1,42 @@ + + * + * 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 19c91120836..8fcd75cf2a5 100644 --- a/src/Symfony/Bundle/Resources/config/api.php +++ b/src/Symfony/Bundle/Resources/config/api.php @@ -47,6 +47,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; @@ -69,7 +70,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); 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 f985c963cdf..c4f3a684611 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -24,13 +24,11 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; 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; /** @@ -38,27 +36,29 @@ */ 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(); - + $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); } }