diff --git a/phpstan.neon b/phpstan.neon
index 84a45a378..57090c94d 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -44,6 +44,9 @@ parameters:
# return type mixed of method ParameterBag::get()"
- 'src/Bundle/Controller/Parameters.php'
+ # ResourceMetadata will need to use the generic T
+ - 'src/Component/src/Metadata/ResourceMetadata.php'
+
ignoreErrors:
# Optional MongoDB ODM support - requires doctrine/mongodb-odm-bundle package
# These classes are only loaded when MongoDB driver is configured and the bundle is installed
@@ -94,6 +97,9 @@ parameters:
# See: src/Bundle/DependencyInjection/Configuration.php:30-37
- '/Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::(end|variableNode|scalarNode)\(\)/'
+ # Symfony Dependency Injection Component can use ReflectionClass type as second argument instead of Reflector one.
+ - '/Parameter #2 \$configurator of method Symfony\\Component\\DependencyInjection\\ContainerBuilder::registerAttributeForAutoconfiguration\(\)/'
+
# Backward compatibility for Symfony DependencyInjection Alias::setDeprecated()
# Method signature changed between Symfony versions:
# - Symfony < 5.1: setDeprecated($status, $message)
diff --git a/psalm.xml b/psalm.xml
index ee44beeb2..cd111bc41 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -39,6 +39,7 @@
+
@@ -153,6 +154,12 @@
+
+
+
+
+
+
diff --git a/src/Bundle/DependencyInjection/SyliusResourceExtension.php b/src/Bundle/DependencyInjection/SyliusResourceExtension.php
index 00b668833..d1503da55 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,32 @@ 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, \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('sylius.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('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 MetadataMutatorPass());
$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 @@
+> */
+ private array $mutators = [];
+
+ /**
+ * Adds a mutator to the container for a given operation name.
+ */
+ public function add(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/Component/src/Metadata/Mutator/OperationMutatorCollectionInterface.php b/src/Component/src/Metadata/Mutator/OperationMutatorCollectionInterface.php
new file mode 100644
index 000000000..fdbb41668
--- /dev/null
+++ b/src/Component/src/Metadata/Mutator/OperationMutatorCollectionInterface.php
@@ -0,0 +1,30 @@
+
+ */
+ public function get(string $id): array;
+}
diff --git a/src/Component/src/Metadata/Mutator/ResourceMutatorCollection.php b/src/Component/src/Metadata/Mutator/ResourceMutatorCollection.php
new file mode 100644
index 000000000..c7d20cfc7
--- /dev/null
+++ b/src/Component/src/Metadata/Mutator/ResourceMutatorCollection.php
@@ -0,0 +1,40 @@
+> */
+ private array $mutators = [];
+
+ public function add(string $resourceClass, ResourceMutatorInterface $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/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php b/src/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php
new file mode 100644
index 000000000..9ad5d58fd
--- /dev/null
+++ b/src/Component/src/Metadata/Mutator/ResourceMutatorCollectionInterface.php
@@ -0,0 +1,30 @@
+
+ */
+ public function get(string $id): array;
+}
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 @@
+ $operations
+ * @param array $operations
*/
public function __construct(array $operations = [])
{
@@ -32,6 +34,9 @@ public function __construct(array $operations = [])
}
}
+ /**
+ * @return \Iterator
+ */
public function getIterator(): \Traversable
{
return (function (): \Generator {
@@ -52,6 +57,11 @@ public function get(string $key): Operation
throw new \RuntimeException(sprintf('No Operation with key "%s" was found', $key));
}
+ /**
+ * @param T $value
+ *
+ * @return self
+ */
public function add(string $key, Operation $value): self
{
foreach ($this->operations as $i => [$operationName, $operation]) {
@@ -67,6 +77,9 @@ public function add(string $key, Operation $value): self
return $this;
}
+ /**
+ * @return self
+ */
public function remove(string $key): self
{
foreach ($this->operations as $i => [$operationName, $operation]) {
diff --git a/src/Component/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php b/src/Component/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php
new file mode 100644
index 000000000..198cc923a
--- /dev/null
+++ b/src/Component/src/Metadata/Resource/Factory/MutatorResourceMetadataCollectionFactory.php
@@ -0,0 +1,83 @@
+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;
+ }
+
+ /**
+ * @template T of Operation
+ *
+ * @param Operations $operations
+ *
+ * @return Operations
+ */
+ private function mutateOperations(Operations $operations): Operations
+ {
+ $newOperations = new Operations();
+
+ 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..4c2837957
--- /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 ResourceMutatorCollection();
+ $resourceMutatorCollection->add($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(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 OperationMutatorCollection();
+ $operationMutatorCollection->add('app_dummy_index', 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(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');
+ }
+}
diff --git a/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php
index 0466acbf8..c57d8ae62 100644
--- a/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php
+++ b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/BoardGameResource.php
@@ -14,7 +14,6 @@
namespace App\BoardGameBlog\Infrastructure\Sylius\Resource;
use App\BoardGameBlog\Domain\Model\BoardGame;
-use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Processor\CreateBoardGameProcessor;
use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Processor\DeleteBoardGameProcessor;
use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Processor\UpdateBoardGameProcessor;
use App\BoardGameBlog\Infrastructure\Sylius\State\Http\Provider\BoardGameCollectionProvider;
@@ -34,13 +33,10 @@
alias: 'app.board_game',
section: 'admin',
formType: BoardGameType::class,
- templatesDir: 'crud',
routePrefix: '/admin',
driver: false,
)]
-#[Create(
- processor: CreateBoardGameProcessor::class,
-)]
+#[Create]
#[Update(
provider: BoardGameItemProvider::class,
processor: UpdateBoardGameProcessor::class,
diff --git a/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/BoardGameTemplatesDirMutator.php b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/BoardGameTemplatesDirMutator.php
new file mode 100644
index 000000000..f54fbdf98
--- /dev/null
+++ b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/BoardGameTemplatesDirMutator.php
@@ -0,0 +1,28 @@
+withTemplatesDir('crud');
+ }
+}
diff --git a/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/CreateBoardGameProcessorMutator.php b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/CreateBoardGameProcessorMutator.php
new file mode 100644
index 000000000..7fd5b075e
--- /dev/null
+++ b/tests/Application/src/BoardGameBlog/Infrastructure/Sylius/Resource/Mutator/CreateBoardGameProcessorMutator.php
@@ -0,0 +1,28 @@
+withProcessor(CreateBoardGameProcessor::class);
+ }
+}