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 @@
+
+
+
+
+
+
+
+
+
+
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');
+ }
+}