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 @@ + + + + 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..105b72dfc --- /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 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); + } +}