diff --git a/psalm.xml b/psalm.xml index eee4ea440..d56012413 100644 --- a/psalm.xml +++ b/psalm.xml @@ -151,6 +151,12 @@ + + + + + + diff --git a/src/Bundle/DependencyInjection/SyliusResourceExtension.php b/src/Bundle/DependencyInjection/SyliusResourceExtension.php index 00b668833..9cc503736 100644 --- a/src/Bundle/DependencyInjection/SyliusResourceExtension.php +++ b/src/Bundle/DependencyInjection/SyliusResourceExtension.php @@ -23,9 +23,13 @@ use Sylius\Component\Resource\Factory\FactoryInterface as LegacyFactoryInterface; use Sylius\Resource\Factory\Factory; use Sylius\Resource\Factory\FactoryInterface; +use Sylius\Resource\Metadata\AsOperationMutator; use Sylius\Resource\Metadata\AsResource; +use Sylius\Resource\Metadata\AsResourceMutator; use Sylius\Resource\Metadata\Metadata; +use Sylius\Resource\Metadata\OperationMutatorInterface; use Sylius\Resource\Metadata\ResourceMetadata; +use Sylius\Resource\Metadata\ResourceMutatorInterface; use Sylius\Resource\Reflection\ClassReflection; use Sylius\Resource\State\ProcessorInterface; use Sylius\Resource\State\ProviderInterface; @@ -34,6 +38,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -76,6 +81,40 @@ public function load(array $configs, ContainerBuilder $container): void $this->loadPersistence($config['drivers'], $config['resources'], $loader, $container); $this->loadResources($config['resources'], $container); + $container->registerAttributeForAutoconfiguration( + AsResourceMutator::class, + 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)); + } + + $definition->addTag('sylius.resource_mutator', [ + 'resourceClass' => $attribute->resourceClass, + ]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsOperationMutator::class, + 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)); + } + + $definition->addTag('sylius.operation_mutator', [ + 'operationName' => $attribute->operationName, + ]); + }, + ); + $container->registerForAutoconfiguration(ProviderInterface::class) ->addTag('sylius.state_provider') ; diff --git a/src/Bundle/Resources/config/services/metadata/extractor.xml b/src/Bundle/Resources/config/services/metadata/extractor.xml index 6b0b6f0a1..7de8fa386 100644 --- a/src/Bundle/Resources/config/services/metadata/extractor.xml +++ b/src/Bundle/Resources/config/services/metadata/extractor.xml @@ -1,3 +1,16 @@ + + + + diff --git a/src/Bundle/Resources/config/services/metadata/mutator.xml b/src/Bundle/Resources/config/services/metadata/mutator.xml new file mode 100644 index 000000000..ab14e80bf --- /dev/null +++ b/src/Bundle/Resources/config/services/metadata/mutator.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/services/metadata/resource_metadata_collection.xml b/src/Bundle/Resources/config/services/metadata/resource_metadata_collection.xml index 41dbae867..9cb5ac27c 100644 --- a/src/Bundle/Resources/config/services/metadata/resource_metadata_collection.xml +++ b/src/Bundle/Resources/config/services/metadata/resource_metadata_collection.xml @@ -36,6 +36,15 @@ + + + + + + addCompilerPass(new FallbackToKernelDefaultLocalePass()); $container->addCompilerPass(new DoctrineContainerRepositoryFactoryPass()); $container->addCompilerPass(new DoctrineTargetEntitiesResolverPass(new TargetEntitiesResolver()), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1); + $container->addCompilerPass(new MutatorPass()); $container->addCompilerPass(new RegisterFormBuilderPass()); $container->addCompilerPass(new RegisterFqcnControllersPass()); $container->addCompilerPass(new RegisterResourceRepositoryPass()); diff --git a/src/Component/src/Metadata/AsOperationMutator.php b/src/Component/src/Metadata/AsOperationMutator.php new file mode 100644 index 000000000..12777d0eb --- /dev/null +++ b/src/Component/src/Metadata/AsOperationMutator.php @@ -0,0 +1,23 @@ + + */ + public function get(string $id): mixed; +} diff --git a/src/Component/src/Metadata/Mutator/OperationResourceMutatorCollection.php b/src/Component/src/Metadata/Mutator/OperationResourceMutatorCollection.php new file mode 100644 index 000000000..7feb42719 --- /dev/null +++ b/src/Component/src/Metadata/Mutator/OperationResourceMutatorCollection.php @@ -0,0 +1,42 @@ +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/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php b/src/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php new file mode 100644 index 000000000..5b90fdafe --- /dev/null +++ b/src/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php @@ -0,0 +1,28 @@ + + */ + public function get(string $id): array; +} diff --git a/src/Component/src/Metadata/Mutator/ResourceResourceMutatorCollection.php b/src/Component/src/Metadata/Mutator/ResourceResourceMutatorCollection.php new file mode 100644 index 000000000..4e4b2b0f7 --- /dev/null +++ b/src/Component/src/Metadata/Mutator/ResourceResourceMutatorCollection.php @@ -0,0 +1,39 @@ +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/Component/src/Metadata/OperationMutatorInterface.php b/src/Component/src/Metadata/OperationMutatorInterface.php new file mode 100644 index 000000000..e15ced7f4 --- /dev/null +++ b/src/Component/src/Metadata/OperationMutatorInterface.php @@ -0,0 +1,19 @@ +decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + $newMetadataCollection = new ResourceMetadataCollection(); + + /** @var ResourceMetadata $resource */ + 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(ResourceMetadata $resource, string $resourceClass): ResourceMetadata + { + foreach ($this->resourceMutators->get($resourceClass) as $mutator) { + $resource = $mutator($resource); + } + + return $resource; + } + + private function mutateOperations(Operations $operations): Operations + { + $newOperations = new Operations(); + + /** + * @var Operation $operation + * @var string $key + */ + 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/Component/src/Metadata/ResourceMutatorInterface.php b/src/Component/src/Metadata/ResourceMutatorInterface.php new file mode 100644 index 000000000..ccaf42a2f --- /dev/null +++ b/src/Component/src/Metadata/ResourceMutatorInterface.php @@ -0,0 +1,19 @@ +processResourceMutators($container); + $this->processOperationMutators($container); + } + + public function processResourceMutators(ContainerBuilder $container): void + { + if (!$container->hasDefinition('sylius.metadata.mutator_collection.resource')) { + return; + } + + $definition = $container->getDefinition('sylius.metadata.mutator_collection.resource'); + + $mutators = $container->findTaggedServiceIds('sylius.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('sylius.metadata.mutator_collection.operation')) { + return; + } + + $definition = $container->getDefinition('sylius.metadata.mutator_collection.operation'); + + $mutators = $container->findTaggedServiceIds('sylius.operation_mutator'); + + foreach ($mutators as $id => $tags) { + foreach ($tags as $tag) { + $definition->addMethodCall('add', [ + $tag['operationName'], + new Reference($id), + ]); + } + } + } +} diff --git a/src/Component/tests/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php b/src/Component/tests/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php new file mode 100644 index 000000000..fc8e8b1d5 --- /dev/null +++ b/src/Component/tests/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactoryTest.php @@ -0,0 +1,96 @@ +createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceClass = \stdClass::class; + $resourceMetadataCollection = new ResourceMetadataCollection(); + $resourceMetadataCollection[] = (new ResourceMetadata())->withClass($resourceClass); + + $resourceMutatorCollection = new ResourceResourceMutatorCollection(); + $resourceMutatorCollection->addMutator($resourceClass, new DummyResourceMutator()); + + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory($resourceMutatorCollection, new OperationResourceMutatorCollection(), $decorated); + + $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( + $resourceMetadataCollection, + ); + + $resourceMetadataCollection = $customResourceMetadataCollectionFactory->create($resourceClass); + + $resource = $resourceMetadataCollection->getIterator()->current(); + $this->assertInstanceOf(ResourceMetadata::class, $resource); + $this->assertSame('custom_dummy', $resource->getName()); + } + + public function testMutateOperation(): void + { + $decorated = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceClass = \stdClass::class; + + $operations = new Operations(); + $operations->add('app_dummy_index', new HttpOperation()); + + $resourceMetadataCollection = new ResourceMetadataCollection(); + $resourceMetadataCollection[] = (new ResourceMetadata(alias: 'app.dummy'))->withClass($resourceClass)->withOperations($operations); + + $operationMutatorCollection = new OperationResourceMutatorCollection(); + $operationMutatorCollection->add('app_dummy_index', new DummyOperationMutator()); + + $customResourceMetadataCollectionFactory = new MutatorResourceMetadataCollectionFactory(new ResourceResourceMutatorCollection(), $operationMutatorCollection, $decorated); + + $decorated->expects($this->once())->method('create')->with($resourceClass)->willReturn( + $resourceMetadataCollection, + ); + + $resourceMetadataCollection = $customResourceMetadataCollectionFactory->create($resourceClass); + + $resource = $resourceMetadataCollection->getIterator()->current(); + $this->assertInstanceOf(ResourceMetadata::class, $resource); + $this->assertEquals('custom_dummy', $resourceMetadataCollection->getOperation('app.dummy', 'app_dummy_index')->getShortName()); + } +} + +final class DummyResourceMutator implements ResourceMutatorInterface +{ + public function __invoke(ResourceMetadata $resource): ResourceMetadata + { + return $resource->withName('custom_dummy'); + } +} + +final class DummyOperationMutator implements OperationMutatorInterface +{ + public function __invoke(Operation $operation): Operation + { + return $operation->withShortName('custom_dummy'); + } +}