From 7f8aa4f7fdb01b73efdc53b969234603da07ecc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 13 Jun 2025 16:17:49 +0200 Subject: [PATCH 1/4] feat(metadata) Customize Resource & operations --- src/Metadata/AsResourceMutator.php | 26 +++++++++++ .../Mutator/ResourceMutatorCollection.php | 36 +++++++++++++++ ...ustomResourceMetadataCollectionFactory.php | 46 +++++++++++++++++++ src/Symfony/Bundle/ApiPlatformBundle.php | 2 + .../ApiPlatformExtension.php | 9 ++++ .../Compiler/MutatorPass.php | 41 +++++++++++++++++ .../Resources/config/metadata/mutator.xml | 10 ++++ .../Resources/config/metadata/resource.xml | 5 ++ .../Symfony/Bundle/ApiPlatformBundleTest.php | 2 + 9 files changed, 177 insertions(+) create mode 100644 src/Metadata/AsResourceMutator.php create mode 100644 src/Metadata/Mutator/ResourceMutatorCollection.php create mode 100644 src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php create mode 100644 src/Symfony/Bundle/Resources/config/metadata/mutator.xml diff --git a/src/Metadata/AsResourceMutator.php b/src/Metadata/AsResourceMutator.php new file mode 100644 index 00000000000..99527b1fa58 --- /dev/null +++ b/src/Metadata/AsResourceMutator.php @@ -0,0 +1,26 @@ + + * + * 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\Metadata; + +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsResourceMutator +{ + /** + * @param class-string $resourceClass + */ + public function __construct( + public readonly string $resourceClass, + ) { + } +} diff --git a/src/Metadata/Mutator/ResourceMutatorCollection.php b/src/Metadata/Mutator/ResourceMutatorCollection.php new file mode 100644 index 00000000000..5ddbcdfb1af --- /dev/null +++ b/src/Metadata/Mutator/ResourceMutatorCollection.php @@ -0,0 +1,36 @@ + + * + * 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\Metadata\Mutator; + +use Psr\Container\ContainerInterface; + +final class ResourceMutatorCollection implements ContainerInterface +{ + private array $mutators; + + public function addMutator(string $resourceClass, object $mutator): void + { + $this->mutators[$resourceClass][] = $mutator; + } + + public function get(string $id): array + { + return $this->mutators[$id] ?? []; + } + + public function has(string $id): bool + { + return isset($this->mutators[$id]); + } +} diff --git a/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..a648198b4b4 --- /dev/null +++ b/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php @@ -0,0 +1,46 @@ + + * + * 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\Metadata\Resource\Factory; + +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Psr\Container\ContainerInterface; + +final class CustomResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private readonly ContainerInterface $resourceMutators, + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + $newMetadataCollection = new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $resource) { + foreach ($this->resourceMutators->get($resourceClass) as $mutators) { + $resource = $mutators($resource); + } + + $newMetadataCollection[] = $resource; + } + + return $newMetadataCollection; + } +} diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 8a3eb9021bf..3996c7e911d 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -22,6 +22,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; 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\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -56,5 +57,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); $container->addCompilerPass(new SerializerMappingLoaderPass()); + $container->addCompilerPass(new MutatorPass()); } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 883a6a411a1..4260d19a9bb 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -31,6 +31,7 @@ use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\AsResourceMutator; use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\UriVariableTransformerInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; @@ -187,6 +188,13 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('api_platform.resource') ->addTag('container.excluded', ['source' => 'by #[ApiResource] attribute']); }); + $container->registerAttributeForAutoconfiguration(AsResourceMutator::class, + static function (ChildDefinition $definition, AsResourceMutator $attribute): void { + $definition->addTag('api_platform.resource_mutator', [ + 'resourceClass' => $attribute->resourceClass, + ]); + }, + ); if (!$container->has('api_platform.state.item_provider')) { $container->setAlias('api_platform.state.item_provider', 'api_platform.state_provider.object'); @@ -345,6 +353,7 @@ private function registerMetadataConfiguration(ContainerBuilder $container, arra $loader->load('metadata/property.xml'); $loader->load('metadata/resource.xml'); $loader->load('metadata/operation.xml'); + $loader->load('metadata/mutator.xml'); $container->getDefinition('api_platform.metadata.resource_extractor.xml')->replaceArgument(0, $xmlResources); $container->getDefinition('api_platform.metadata.property_extractor.xml')->replaceArgument(0, $xmlResources); diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php new file mode 100644 index 00000000000..d709bafc052 --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php @@ -0,0 +1,41 @@ + + * + * 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\Reference; + +class MutatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('api_platform.metadata.mutator_collection.resource')) { + return; + } + + $definition = $container->getDefinition('api_platform.metadata.mutator_collection.resource'); + + $mutators = $container->findTaggedServiceIds('api_platform.resource_mutator'); + + foreach ($mutators as $id => $tags) { + foreach ($tags as $tag) { + $definition->addMethodCall('addMutator', [ + $tag['resourceClass'], + new Reference($id), + ]); + } + } + } +} diff --git a/src/Symfony/Bundle/Resources/config/metadata/mutator.xml b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml new file mode 100644 index 00000000000..877f8e62328 --- /dev/null +++ b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index f0caa4147b8..548101d9460 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -30,6 +30,11 @@ + + + + + diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 070a6b35af0..a189f93675c 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -23,6 +23,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; 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\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -54,6 +55,7 @@ public function testBuild(): void $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(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal()); From 345e38cb01e6654c3c86702a3c9ae8bf21ddfc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Fri, 13 Jun 2025 17:15:10 +0200 Subject: [PATCH 2/4] Add Operation mutator --- src/Metadata/AsOperationMutator.php | 23 +++++ .../Mutator/OperationMutatorCollection.php | 37 +++++++ .../Mutator/ResourceMutatorCollection.php | 3 +- src/Metadata/OperationMutatorInterface.php | 19 ++++ ...ustomResourceMetadataCollectionFactory.php | 46 --------- ...tatorResourceMetadataCollectionFactory.php | 81 ++++++++++++++++ src/Metadata/ResourceMutatorInterface.php | 19 ++++ ...rResourceMetadataCollectionFactoryTest.php | 96 +++++++++++++++++++ src/Symfony/Bundle/ApiPlatformBundle.php | 6 +- .../ApiPlatformExtension.php | 21 +++- .../Compiler/OperationMutatorPass.php | 41 ++++++++ ...utatorPass.php => ResourceMutatorPass.php} | 2 +- .../Resources/config/metadata/mutator.xml | 2 + .../Resources/config/metadata/resource.xml | 5 +- .../Symfony/Bundle/ApiPlatformBundleTest.php | 6 +- 15 files changed, 352 insertions(+), 55 deletions(-) create mode 100644 src/Metadata/AsOperationMutator.php create mode 100644 src/Metadata/Mutator/OperationMutatorCollection.php create mode 100644 src/Metadata/OperationMutatorInterface.php delete mode 100644 src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php create mode 100644 src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php create mode 100644 src/Metadata/ResourceMutatorInterface.php create mode 100644 src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php create mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php rename src/Symfony/Bundle/DependencyInjection/Compiler/{MutatorPass.php => ResourceMutatorPass.php} (95%) diff --git a/src/Metadata/AsOperationMutator.php b/src/Metadata/AsOperationMutator.php new file mode 100644 index 00000000000..24007193446 --- /dev/null +++ b/src/Metadata/AsOperationMutator.php @@ -0,0 +1,23 @@ + + * + * 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\Metadata; + +#[\Attribute(\Attribute::TARGET_CLASS)] +final class AsOperationMutator +{ + public function __construct( + public readonly string $operationName, + ) { + } +} diff --git a/src/Metadata/Mutator/OperationMutatorCollection.php b/src/Metadata/Mutator/OperationMutatorCollection.php new file mode 100644 index 00000000000..b329dd2de56 --- /dev/null +++ b/src/Metadata/Mutator/OperationMutatorCollection.php @@ -0,0 +1,37 @@ + + * + * 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\Metadata\Mutator; + +use ApiPlatform\Metadata\OperationMutatorInterface; +use Psr\Container\ContainerInterface; + +final class OperationMutatorCollection implements ContainerInterface +{ + private array $mutators; + + public function addMutator(string $operationName, OperationMutatorInterface $mutator): void + { + $this->mutators[$operationName][] = $mutator; + } + + public function get(string $id): array + { + return $this->mutators[$id] ?? []; + } + + public function has(string $id): bool + { + return isset($this->mutators[$id]); + } +} diff --git a/src/Metadata/Mutator/ResourceMutatorCollection.php b/src/Metadata/Mutator/ResourceMutatorCollection.php index 5ddbcdfb1af..c619c5ddddd 100644 --- a/src/Metadata/Mutator/ResourceMutatorCollection.php +++ b/src/Metadata/Mutator/ResourceMutatorCollection.php @@ -13,13 +13,14 @@ namespace ApiPlatform\Metadata\Mutator; +use ApiPlatform\Metadata\ResourceMutatorInterface; use Psr\Container\ContainerInterface; final class ResourceMutatorCollection implements ContainerInterface { private array $mutators; - public function addMutator(string $resourceClass, object $mutator): void + public function addMutator(string $resourceClass, ResourceMutatorInterface $mutator): void { $this->mutators[$resourceClass][] = $mutator; } diff --git a/src/Metadata/OperationMutatorInterface.php b/src/Metadata/OperationMutatorInterface.php new file mode 100644 index 00000000000..98cde4ba6c1 --- /dev/null +++ b/src/Metadata/OperationMutatorInterface.php @@ -0,0 +1,19 @@ + + * + * 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\Metadata; + +interface OperationMutatorInterface +{ + public function __invoke(Operation $operation): Operation; +} diff --git a/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php deleted file mode 100644 index a648198b4b4..00000000000 --- a/src/Metadata/Resource/Factory/CustomResourceMetadataCollectionFactory.php +++ /dev/null @@ -1,46 +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\Metadata\Resource\Factory; - -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Psr\Container\ContainerInterface; - -final class CustomResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface -{ - public function __construct( - private readonly ContainerInterface $resourceMutators, - private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, - ) { - } - - public function create(string $resourceClass): ResourceMetadataCollection - { - $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); - if ($this->decorated) { - $resourceMetadataCollection = $this->decorated->create($resourceClass); - } - - $newMetadataCollection = new ResourceMetadataCollection($resourceClass); - - foreach ($resourceMetadataCollection as $resource) { - foreach ($this->resourceMutators->get($resourceClass) as $mutators) { - $resource = $mutators($resource); - } - - $newMetadataCollection[] = $resource; - } - - return $newMetadataCollection; - } -} diff --git a/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..954497aa6bd --- /dev/null +++ b/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php @@ -0,0 +1,81 @@ + + * + * 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\Metadata\Resource\Factory; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\OperationMutatorInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceMutatorInterface; +use Psr\Container\ContainerInterface; + +final class MutatorResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + /** + * @param ContainerInterface $resourceMutators + * @param ContainerInterface $operationMutators + */ + public function __construct( + private readonly ContainerInterface $resourceMutators, + private readonly ContainerInterface $operationMutators, + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + $newMetadataCollection = new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $resource) { + $resource = $this->mutateResource($resource, $resourceClass); + $operations = $this->mutateOperations($resource->getOperations() ?? new Operations()); + $resource = $resource->withOperations($operations); + + $newMetadataCollection[] = $resource; + } + + return $newMetadataCollection; + } + + private function mutateResource(ApiResource $resource, string $resourceClass): ApiResource + { + foreach ($this->resourceMutators->get($resourceClass) as $mutator) { + $resource = $mutator($resource); + } + + return $resource; + } + + private function mutateOperations(Operations $operations): Operations + { + $newOperations = new Operations(); + + /** @var Operation $operation */ + foreach ($operations as $key => $operation) { + foreach ($this->operationMutators->get($key) as $mutator) { + $operation = $mutator($operation); + } + + $newOperations->add($key, $operation); + } + + return $newOperations; + } +} diff --git a/src/Metadata/ResourceMutatorInterface.php b/src/Metadata/ResourceMutatorInterface.php new file mode 100644 index 00000000000..fb7f7efce2d --- /dev/null +++ b/src/Metadata/ResourceMutatorInterface.php @@ -0,0 +1,19 @@ + + * + * 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\Metadata; + +interface ResourceMutatorInterface +{ + public function __invoke(ApiResource $resource): ApiResource; +} diff --git a/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php new file mode 100644 index 00000000000..66ce11a79bf --- /dev/null +++ b/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php @@ -0,0 +1,96 @@ + + * + * 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\Metadata\Tests\Resource\Factory; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Mutator\OperationMutatorCollection; +use ApiPlatform\Metadata\Mutator\ResourceMutatorCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\OperationMutatorInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\MutatorResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceMutatorInterface; +use PHPUnit\Framework\TestCase; + +final class MutatorResourceMetadataCollectionFactoryTest extends TestCase +{ + public function testMutateResource(): void + { + $decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceClass = \stdClass::class; + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + $resourceMetadataCollection[] = (new ApiResource())->withClass($resourceClass); + + $resourceMutatorCollection = new ResourceMutatorCollection(); + $resourceMutatorCollection->addMutator($resourceClass, new DummyResourceMutator()); + + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory($resourceMutatorCollection, new OperationMutatorCollection(), $decorated); + + $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( + $resourceMetadataCollection, + ); + + $resourceMetadataCollection = $customResourceMetadataCollectionFactory->create($resourceClass); + + $resource = $resourceMetadataCollection->getIterator()->current(); + $this->assertInstanceOf(ApiResource::class, $resource); + $this->assertSame('dummy', $resource->getShortName()); + } + + public function testMutateOperation(): void + { + $decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceClass = \stdClass::class; + + $operations = new Operations(); + $operations->add('_api_Dummy_get', new HttpOperation()); + + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + $resourceMetadataCollection[] = (new ApiResource())->withClass($resourceClass)->withOperations($operations); + + $operationMutatorCollection = new OperationMutatorCollection(); + $operationMutatorCollection->addMutator('_api_Dummy_get', new DummyOperationMutator()); + + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory(new ResourceMutatorCollection(), $operationMutatorCollection, $decorated); + + $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( + $resourceMetadataCollection, + ); + + $resourceMetadataCollection = $customResourceMetadataCollectionFactory->create($resourceClass); + + $resource = $resourceMetadataCollection->getIterator()->current(); + $this->assertInstanceOf(ApiResource::class, $resource); + $this->assertEquals('custom_dummy', $resourceMetadataCollection->getOperation('_api_Dummy_get')->getShortName()); + } +} + +final class DummyResourceMutator implements ResourceMutatorInterface +{ + public function __invoke(ApiResource $resource): ApiResource + { + return $resource->withShortName('dummy'); + } +} + +final class DummyOperationMutator implements OperationMutatorInterface +{ + public function __invoke(Operation $operation): Operation + { + return $operation->withShortName('custom_dummy'); + } +} diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index 3996c7e911d..bede21a1efa 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -22,7 +22,8 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; 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\OperationMutatorPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ResourceMutatorPass; 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 TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); $container->addCompilerPass(new SerializerMappingLoaderPass()); - $container->addCompilerPass(new MutatorPass()); + $container->addCompilerPass(new ResourceMutatorPass()); + $container->addCompilerPass(new OperationMutatorPass()); } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 4260d19a9bb..18163d9a8e6 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -31,8 +31,11 @@ use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface as GraphQlTypeInterface; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\AsOperationMutator; use ApiPlatform\Metadata\AsResourceMutator; use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\OperationMutatorInterface; +use ApiPlatform\Metadata\ResourceMutatorInterface; use ApiPlatform\Metadata\UriVariableTransformerInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\OpenApi\Model\Tag; @@ -189,13 +192,29 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('container.excluded', ['source' => 'by #[ApiResource] attribute']); }); $container->registerAttributeForAutoconfiguration(AsResourceMutator::class, - static function (ChildDefinition $definition, AsResourceMutator $attribute): void { + static function (ChildDefinition $definition, AsResourceMutator $attribute, \ReflectionClass $reflector): void { + if (!is_a($reflector->name, ResourceMutatorInterface::class, true)) { + throw new RuntimeException(\sprintf('Resource mutator "%s" should implement %s', $reflector->name, ResourceMutatorInterface::class)); + } + $definition->addTag('api_platform.resource_mutator', [ 'resourceClass' => $attribute->resourceClass, ]); }, ); + $container->registerAttributeForAutoconfiguration(AsOperationMutator::class, + static function (ChildDefinition $definition, AsOperationMutator $attribute, \ReflectionClass $reflector): void { + if (!is_a($reflector->name, OperationMutatorInterface::class, true)) { + throw new RuntimeException(\sprintf('Operation mutator "%s" should implement %s', $reflector->name, OperationMutatorInterface::class)); + } + + $definition->addTag('api_platform.operation_mutator', [ + 'operationName' => $attribute->operationName, + ]); + }, + ); + if (!$container->has('api_platform.state.item_provider')) { $container->setAlias('api_platform.state.item_provider', 'api_platform.state_provider.object'); } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php new file mode 100644 index 00000000000..1f94cff2acb --- /dev/null +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php @@ -0,0 +1,41 @@ + + * + * 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\Reference; + +class OperationMutatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('api_platform.metadata.mutator_collection.operation')) { + return; + } + + $definition = $container->getDefinition('api_platform.metadata.mutator_collection.operation'); + + $mutators = $container->findTaggedServiceIds('api_platform.operation_mutator'); + + foreach ($mutators as $id => $tags) { + foreach ($tags as $tag) { + $definition->addMethodCall('addMutator', [ + $tag['operationName'], + new Reference($id), + ]); + } + } + } +} diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php similarity index 95% rename from src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php rename to src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php index d709bafc052..159a7687940 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php @@ -17,7 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -class MutatorPass implements CompilerPassInterface +class ResourceMutatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/src/Symfony/Bundle/Resources/config/metadata/mutator.xml b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml index 877f8e62328..be13ef10490 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/mutator.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml @@ -6,5 +6,7 @@ + + diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index 548101d9460..a36de0c8331 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -30,9 +30,10 @@ - + - + + diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index a189f93675c..33bd4a1d158 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -23,7 +23,8 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; 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\OperationMutatorPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ResourceMutatorPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -55,7 +56,8 @@ public function testBuild(): void $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(ResourceMutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(OperationMutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal()); From 81e0cecb2ab45bc5f083e782748cd6667558036b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Wed, 2 Jul 2025 18:32:04 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Antoine Bluchet --- src/Metadata/AsOperationMutator.php | 2 +- src/Metadata/AsResourceMutator.php | 2 +- src/Metadata/Mutator/OperationMutatorCollection.php | 7 +++++-- .../DependencyInjection/Compiler/OperationMutatorPass.php | 2 +- .../DependencyInjection/Compiler/ResourceMutatorPass.php | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Metadata/AsOperationMutator.php b/src/Metadata/AsOperationMutator.php index 24007193446..737b993eebb 100644 --- a/src/Metadata/AsOperationMutator.php +++ b/src/Metadata/AsOperationMutator.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Metadata; #[\Attribute(\Attribute::TARGET_CLASS)] -final class AsOperationMutator +class AsOperationMutator { public function __construct( public readonly string $operationName, diff --git a/src/Metadata/AsResourceMutator.php b/src/Metadata/AsResourceMutator.php index 99527b1fa58..fb3718cc364 100644 --- a/src/Metadata/AsResourceMutator.php +++ b/src/Metadata/AsResourceMutator.php @@ -14,7 +14,7 @@ namespace ApiPlatform\Metadata; #[\Attribute(\Attribute::TARGET_CLASS)] -final class AsResourceMutator +class AsResourceMutator { /** * @param class-string $resourceClass diff --git a/src/Metadata/Mutator/OperationMutatorCollection.php b/src/Metadata/Mutator/OperationMutatorCollection.php index b329dd2de56..9846eb306f8 100644 --- a/src/Metadata/Mutator/OperationMutatorCollection.php +++ b/src/Metadata/Mutator/OperationMutatorCollection.php @@ -18,9 +18,12 @@ final class OperationMutatorCollection implements ContainerInterface { - private array $mutators; + private array $mutators = []; - public function addMutator(string $operationName, OperationMutatorInterface $mutator): void + /** + * Adds a mutator to the container for a given operation name. + */ + public function add(string $operationName, OperationMutatorInterface $mutator): void { $this->mutators[$operationName][] = $mutator; } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php index 1f94cff2acb..7949d2651aa 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php @@ -31,7 +31,7 @@ public function process(ContainerBuilder $container): void foreach ($mutators as $id => $tags) { foreach ($tags as $tag) { - $definition->addMethodCall('addMutator', [ + $definition->addMethodCall('add', [ $tag['operationName'], new Reference($id), ]); diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php index 159a7687940..4a5725febf6 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php @@ -31,7 +31,7 @@ public function process(ContainerBuilder $container): void foreach ($mutators as $id => $tags) { foreach ($tags as $tag) { - $definition->addMethodCall('addMutator', [ + $definition->addMethodCall('add', [ $tag['resourceClass'], new Reference($id), ]); From d4ecfb6099745c0ea11eea10a6fe5e344283dbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Wed, 2 Jul 2025 18:55:27 +0200 Subject: [PATCH 4/4] Merge mutator pass & new mutator collection interfaces --- .../OperationMutatorCollectionInterface.php | 28 +++++++++++++ ...=> OperationResourceMutatorCollection.php} | 6 ++- .../ResourceMutatorCollectionInterface.php | 28 +++++++++++++ ... => ResourceResourceMutatorCollection.php} | 6 ++- ...tatorResourceMetadataCollectionFactory.php | 13 ++---- ...rResourceMetadataCollectionFactoryTest.php | 14 +++---- src/Symfony/Bundle/ApiPlatformBundle.php | 6 +-- .../ApiPlatformExtension.php | 12 +++++- ...erationMutatorPass.php => MutatorPass.php} | 28 ++++++++++++- .../Compiler/ResourceMutatorPass.php | 41 ------------------- .../Resources/config/metadata/mutator.xml | 4 +- .../Symfony/Bundle/ApiPlatformBundleTest.php | 6 +-- 12 files changed, 118 insertions(+), 74 deletions(-) create mode 100644 src/Metadata/Mutator/OperationMutatorCollectionInterface.php rename src/Metadata/Mutator/{OperationMutatorCollection.php => OperationResourceMutatorCollection.php} (87%) create mode 100644 src/Metadata/Mutator/ResourceMutatorCollectionInterface.php rename src/Metadata/Mutator/{ResourceMutatorCollection.php => ResourceResourceMutatorCollection.php} (86%) rename src/Symfony/Bundle/DependencyInjection/Compiler/{OperationMutatorPass.php => MutatorPass.php} (56%) delete mode 100644 src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php diff --git a/src/Metadata/Mutator/OperationMutatorCollectionInterface.php b/src/Metadata/Mutator/OperationMutatorCollectionInterface.php new file mode 100644 index 00000000000..d02f31e6c8d --- /dev/null +++ b/src/Metadata/Mutator/OperationMutatorCollectionInterface.php @@ -0,0 +1,28 @@ + + * + * 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\Metadata\Mutator; + +use ApiPlatform\Metadata\OperationMutatorInterface; +use Psr\Container\ContainerInterface; + +/** + * Collection of Operation mutators to mutate Operation metadata. + */ +interface OperationMutatorCollectionInterface extends ContainerInterface +{ + /** + * @return list + */ + public function get(string $id): mixed; +} diff --git a/src/Metadata/Mutator/OperationMutatorCollection.php b/src/Metadata/Mutator/OperationResourceMutatorCollection.php similarity index 87% rename from src/Metadata/Mutator/OperationMutatorCollection.php rename to src/Metadata/Mutator/OperationResourceMutatorCollection.php index 9846eb306f8..c897f67567b 100644 --- a/src/Metadata/Mutator/OperationMutatorCollection.php +++ b/src/Metadata/Mutator/OperationResourceMutatorCollection.php @@ -14,9 +14,11 @@ namespace ApiPlatform\Metadata\Mutator; use ApiPlatform\Metadata\OperationMutatorInterface; -use Psr\Container\ContainerInterface; -final class OperationMutatorCollection implements ContainerInterface +/** + * @internal + */ +final class OperationResourceMutatorCollection implements OperationMutatorCollectionInterface { private array $mutators = []; diff --git a/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php b/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php new file mode 100644 index 00000000000..17dc3d41748 --- /dev/null +++ b/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php @@ -0,0 +1,28 @@ + + * + * 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\Metadata\Mutator; + +use ApiPlatform\Metadata\ResourceMutatorInterface; +use Psr\Container\ContainerInterface; + +/** + * Collection of Resource mutators to mutate ApiResource metadata. + */ +interface ResourceMutatorCollectionInterface extends ContainerInterface +{ + /** + * @return list + */ + public function get(string $id): array; +} diff --git a/src/Metadata/Mutator/ResourceMutatorCollection.php b/src/Metadata/Mutator/ResourceResourceMutatorCollection.php similarity index 86% rename from src/Metadata/Mutator/ResourceMutatorCollection.php rename to src/Metadata/Mutator/ResourceResourceMutatorCollection.php index c619c5ddddd..5add72a233e 100644 --- a/src/Metadata/Mutator/ResourceMutatorCollection.php +++ b/src/Metadata/Mutator/ResourceResourceMutatorCollection.php @@ -14,9 +14,11 @@ namespace ApiPlatform\Metadata\Mutator; use ApiPlatform\Metadata\ResourceMutatorInterface; -use Psr\Container\ContainerInterface; -final class ResourceMutatorCollection implements ContainerInterface +/** + * @internal + */ +final class ResourceResourceMutatorCollection implements ResourceMutatorCollectionInterface { private array $mutators; diff --git a/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php index 954497aa6bd..0afd63200fe 100644 --- a/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php @@ -14,22 +14,17 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Mutator\OperationMutatorCollectionInterface; +use ApiPlatform\Metadata\Mutator\ResourceMutatorCollectionInterface; use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\OperationMutatorInterface; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\ResourceMutatorInterface; -use Psr\Container\ContainerInterface; final class MutatorResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - /** - * @param ContainerInterface $resourceMutators - * @param ContainerInterface $operationMutators - */ public function __construct( - private readonly ContainerInterface $resourceMutators, - private readonly ContainerInterface $operationMutators, + private readonly ResourceMutatorCollectionInterface $resourceMutators, + private readonly OperationMutatorCollectionInterface $operationMutators, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ) { } diff --git a/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php b/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php index 66ce11a79bf..7524793d506 100644 --- a/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php +++ b/src/Metadata/Tests/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php @@ -15,8 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Mutator\OperationMutatorCollection; -use ApiPlatform\Metadata\Mutator\ResourceMutatorCollection; +use ApiPlatform\Metadata\Mutator\OperationResourceMutatorCollection; +use ApiPlatform\Metadata\Mutator\ResourceResourceMutatorCollection; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\OperationMutatorInterface; use ApiPlatform\Metadata\Operations; @@ -35,10 +35,10 @@ public function testMutateResource(): void $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); $resourceMetadataCollection[] = (new ApiResource())->withClass($resourceClass); - $resourceMutatorCollection = new ResourceMutatorCollection(); + $resourceMutatorCollection = new ResourceResourceMutatorCollection(); $resourceMutatorCollection->addMutator($resourceClass, new DummyResourceMutator()); - $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory($resourceMutatorCollection, new OperationMutatorCollection(), $decorated); + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory($resourceMutatorCollection, new OperationResourceMutatorCollection(), $decorated); $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( $resourceMetadataCollection, @@ -62,10 +62,10 @@ public function testMutateOperation(): void $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); $resourceMetadataCollection[] = (new ApiResource())->withClass($resourceClass)->withOperations($operations); - $operationMutatorCollection = new OperationMutatorCollection(); - $operationMutatorCollection->addMutator('_api_Dummy_get', new DummyOperationMutator()); + $operationMutatorCollection = new OperationResourceMutatorCollection(); + $operationMutatorCollection->add('_api_Dummy_get', new DummyOperationMutator()); - $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory(new ResourceMutatorCollection(), $operationMutatorCollection, $decorated); + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory(new ResourceResourceMutatorCollection(), $operationMutatorCollection, $decorated); $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( $resourceMetadataCollection, diff --git a/src/Symfony/Bundle/ApiPlatformBundle.php b/src/Symfony/Bundle/ApiPlatformBundle.php index bede21a1efa..3996c7e911d 100644 --- a/src/Symfony/Bundle/ApiPlatformBundle.php +++ b/src/Symfony/Bundle/ApiPlatformBundle.php @@ -22,8 +22,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\OperationMutatorPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ResourceMutatorPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass; 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 build(ContainerBuilder $container): void $container->addCompilerPass(new TestMercureHubPass()); $container->addCompilerPass(new AuthenticatorManagerPass()); $container->addCompilerPass(new SerializerMappingLoaderPass()); - $container->addCompilerPass(new ResourceMutatorPass()); - $container->addCompilerPass(new OperationMutatorPass()); + $container->addCompilerPass(new MutatorPass()); } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 18163d9a8e6..45136e4d3dd 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -192,7 +192,11 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('container.excluded', ['source' => 'by #[ApiResource] attribute']); }); $container->registerAttributeForAutoconfiguration(AsResourceMutator::class, - static function (ChildDefinition $definition, AsResourceMutator $attribute, \ReflectionClass $reflector): void { + static function (ChildDefinition $definition, AsResourceMutator $attribute, \Reflector $reflector): void { + if (!$reflector instanceof \ReflectionClass) { + return; + } + if (!is_a($reflector->name, ResourceMutatorInterface::class, true)) { throw new RuntimeException(\sprintf('Resource mutator "%s" should implement %s', $reflector->name, ResourceMutatorInterface::class)); } @@ -204,7 +208,11 @@ static function (ChildDefinition $definition, AsResourceMutator $attribute, \Ref ); $container->registerAttributeForAutoconfiguration(AsOperationMutator::class, - static function (ChildDefinition $definition, AsOperationMutator $attribute, \ReflectionClass $reflector): void { + static function (ChildDefinition $definition, AsOperationMutator $attribute, \Reflector $reflector): void { + if (!$reflector instanceof \ReflectionClass) { + return; + } + if (!is_a($reflector->name, OperationMutatorInterface::class, true)) { throw new RuntimeException(\sprintf('Operation mutator "%s" should implement %s', $reflector->name, OperationMutatorInterface::class)); } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php similarity index 56% rename from src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php rename to src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php index 7949d2651aa..adc4c399d4b 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/OperationMutatorPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/MutatorPass.php @@ -17,9 +17,35 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -class OperationMutatorPass implements CompilerPassInterface +final class MutatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void + { + $this->processResourceMutators($container); + $this->processOperationMutators($container); + } + + public function processResourceMutators(ContainerBuilder $container): void + { + if (!$container->hasDefinition('api_platform.metadata.mutator_collection.resource')) { + return; + } + + $definition = $container->getDefinition('api_platform.metadata.mutator_collection.resource'); + + $mutators = $container->findTaggedServiceIds('api_platform.resource_mutator'); + + foreach ($mutators as $id => $tags) { + foreach ($tags as $tag) { + $definition->addMethodCall('add', [ + $tag['resourceClass'], + new Reference($id), + ]); + } + } + } + + private function processOperationMutators(ContainerBuilder $container): void { if (!$container->hasDefinition('api_platform.metadata.mutator_collection.operation')) { return; diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php deleted file mode 100644 index 4a5725febf6..00000000000 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/ResourceMutatorPass.php +++ /dev/null @@ -1,41 +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\Reference; - -class ResourceMutatorPass implements CompilerPassInterface -{ - public function process(ContainerBuilder $container): void - { - if (!$container->hasDefinition('api_platform.metadata.mutator_collection.resource')) { - return; - } - - $definition = $container->getDefinition('api_platform.metadata.mutator_collection.resource'); - - $mutators = $container->findTaggedServiceIds('api_platform.resource_mutator'); - - foreach ($mutators as $id => $tags) { - foreach ($tags as $tag) { - $definition->addMethodCall('add', [ - $tag['resourceClass'], - new Reference($id), - ]); - } - } - } -} diff --git a/src/Symfony/Bundle/Resources/config/metadata/mutator.xml b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml index be13ef10490..bbb78bfbd3a 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/mutator.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/mutator.xml @@ -5,8 +5,8 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + - + diff --git a/tests/Symfony/Bundle/ApiPlatformBundleTest.php b/tests/Symfony/Bundle/ApiPlatformBundleTest.php index 33bd4a1d158..a189f93675c 100644 --- a/tests/Symfony/Bundle/ApiPlatformBundleTest.php +++ b/tests/Symfony/Bundle/ApiPlatformBundleTest.php @@ -23,8 +23,7 @@ use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlResolverPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\OperationMutatorPass; -use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\ResourceMutatorPass; +use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\MutatorPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\SerializerMappingLoaderPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestClientPass; use ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler\TestMercureHubPass; @@ -56,8 +55,7 @@ public function testBuild(): void $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(ResourceMutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); - $containerProphecy->addCompilerPass(Argument::type(OperationMutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); + $containerProphecy->addCompilerPass(Argument::type(MutatorPass::class))->willReturn($containerProphecy->reveal())->shouldBeCalled(); $bundle = new ApiPlatformBundle(); $bundle->build($containerProphecy->reveal());