diff --git a/psalm.xml b/psalm.xml index 926a6e2ad..e30226106 100644 --- a/psalm.xml +++ b/psalm.xml @@ -159,6 +159,7 @@ + diff --git a/src/Bundle/Resources/config/services.xml b/src/Bundle/Resources/config/services.xml index 752db6e81..646e499b0 100644 --- a/src/Bundle/Resources/config/services.xml +++ b/src/Bundle/Resources/config/services.xml @@ -17,6 +17,7 @@ + diff --git a/src/Bundle/Resources/config/services/expression_language.xml b/src/Bundle/Resources/config/services/expression_language.xml new file mode 100644 index 000000000..1e50d98a2 --- /dev/null +++ b/src/Bundle/Resources/config/services/expression_language.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bundle/Resources/config/services/routing.xml b/src/Bundle/Resources/config/services/routing.xml index 8ed70bc43..b1e851db6 100644 --- a/src/Bundle/Resources/config/services/routing.xml +++ b/src/Bundle/Resources/config/services/routing.xml @@ -109,15 +109,9 @@ - - - - - - - + diff --git a/src/Bundle/Resources/config/services/state.xml b/src/Bundle/Resources/config/services/state.xml index 8975cb633..f943aff00 100644 --- a/src/Bundle/Resources/config/services/state.xml +++ b/src/Bundle/Resources/config/services/state.xml @@ -13,15 +13,6 @@ - - - - - - - - - @@ -38,7 +29,7 @@ - + @@ -81,6 +72,7 @@ + diff --git a/src/Component/Metadata/Api/Delete.php b/src/Component/Metadata/Api/Delete.php index 4784a7091..1f52433b6 100644 --- a/src/Component/Metadata/Api/Delete.php +++ b/src/Component/Metadata/Api/Delete.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?bool $read = null, ?bool $write = null, ?bool $validate = null, @@ -58,6 +59,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, read: $read, write: $write, validate: $validate, diff --git a/src/Component/Metadata/Api/Get.php b/src/Component/Metadata/Api/Get.php index 958476150..640a94eb4 100644 --- a/src/Component/Metadata/Api/Get.php +++ b/src/Component/Metadata/Api/Get.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Api/GetCollection.php b/src/Component/Metadata/Api/GetCollection.php index ddda909c6..7d1217d43 100644 --- a/src/Component/Metadata/Api/GetCollection.php +++ b/src/Component/Metadata/Api/GetCollection.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Api/Patch.php b/src/Component/Metadata/Api/Patch.php index 640961133..e6cd16798 100644 --- a/src/Component/Metadata/Api/Patch.php +++ b/src/Component/Metadata/Api/Patch.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Api/Post.php b/src/Component/Metadata/Api/Post.php index 584227052..2c9133f5f 100644 --- a/src/Component/Metadata/Api/Post.php +++ b/src/Component/Metadata/Api/Post.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Api/Put.php b/src/Component/Metadata/Api/Put.php index e9b6ca5bb..1560df9a3 100644 --- a/src/Component/Metadata/Api/Put.php +++ b/src/Component/Metadata/Api/Put.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/BulkDelete.php b/src/Component/Metadata/BulkDelete.php index 3de7bf8d5..afb6cedf8 100644 --- a/src/Component/Metadata/BulkDelete.php +++ b/src/Component/Metadata/BulkDelete.php @@ -30,6 +30,7 @@ public function __construct( string|callable|null $processor = null, string|callable|null $responder = null, string|callable|null $repository = null, + ?array $repositoryArguments = null, ?string $repositoryMethod = null, ?bool $read = null, ?bool $write = null, @@ -51,6 +52,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, read: $read, write: $write, formType: $formType, diff --git a/src/Component/Metadata/Create.php b/src/Component/Metadata/Create.php index e639e51bf..77e8ca14b 100644 --- a/src/Component/Metadata/Create.php +++ b/src/Component/Metadata/Create.php @@ -33,6 +33,7 @@ public function __construct( string|callable|null $processor = null, string|callable|null $responder = null, string|callable|null $repository = null, + ?array $repositoryArguments = null, ?string $repositoryMethod = null, string|callable|false|null $factory = null, private ?string $factoryMethod = null, @@ -65,6 +66,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Delete.php b/src/Component/Metadata/Delete.php index 76874382c..f0d8bfba2 100644 --- a/src/Component/Metadata/Delete.php +++ b/src/Component/Metadata/Delete.php @@ -31,6 +31,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?bool $read = null, ?bool $write = null, ?bool $validate = null, @@ -55,6 +56,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, read: $read, write: $write, validate: $validate, diff --git a/src/Component/Metadata/HttpOperation.php b/src/Component/Metadata/HttpOperation.php index a0ac45ffd..ce6a432b3 100644 --- a/src/Component/Metadata/HttpOperation.php +++ b/src/Component/Metadata/HttpOperation.php @@ -31,6 +31,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -55,6 +56,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Index.php b/src/Component/Metadata/Index.php index ddeb3b602..b2d891de4 100644 --- a/src/Component/Metadata/Index.php +++ b/src/Component/Metadata/Index.php @@ -31,6 +31,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -55,6 +56,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Operation.php b/src/Component/Metadata/Operation.php index 3ae8850f0..b66b9bd72 100644 --- a/src/Component/Metadata/Operation.php +++ b/src/Component/Metadata/Operation.php @@ -41,6 +41,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, protected ?string $repositoryMethod = null, + protected ?array $repositoryArguments = null, protected ?string $grid = null, protected ?bool $read = null, protected ?bool $write = null, @@ -179,6 +180,19 @@ public function withRepositoryMethod(string $repositoryMethod): self return $self; } + public function getRepositoryArguments(): ?array + { + return $this->repositoryArguments; + } + + public function withRepositoryArguments(array $repositoryArguments): self + { + $self = clone $this; + $self->repositoryArguments = $repositoryArguments; + + return $self; + } + public function getGrid(): ?string { return $this->grid; diff --git a/src/Component/Metadata/Show.php b/src/Component/Metadata/Show.php index ea74af822..f46f4e3ab 100644 --- a/src/Component/Metadata/Show.php +++ b/src/Component/Metadata/Show.php @@ -31,6 +31,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -55,6 +56,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/Metadata/Update.php b/src/Component/Metadata/Update.php index 18a80acbf..866228bb7 100644 --- a/src/Component/Metadata/Update.php +++ b/src/Component/Metadata/Update.php @@ -31,6 +31,7 @@ public function __construct( string|callable|null $responder = null, string|callable|null $repository = null, ?string $repositoryMethod = null, + ?array $repositoryArguments = null, ?string $grid = null, ?bool $read = null, ?bool $write = null, @@ -59,6 +60,7 @@ public function __construct( responder: $responder, repository: $repository, repositoryMethod: $repositoryMethod, + repositoryArguments: $repositoryArguments, grid: $grid, read: $read, write: $write, diff --git a/src/Component/State/Factory.php b/src/Component/State/Factory.php index f4ae41f32..ee21d201f 100644 --- a/src/Component/State/Factory.php +++ b/src/Component/State/Factory.php @@ -15,10 +15,10 @@ use Psr\Container\ContainerInterface; use Sylius\Component\Resource\Context\Context; -use Sylius\Component\Resource\Factory\ArgumentParserInterface; use Sylius\Component\Resource\Factory\FactoryInterface as ResourceFactoryInterface; use Sylius\Component\Resource\Metadata\FactoryAwareOperationInterface; use Sylius\Component\Resource\Metadata\Operation; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Webmozart\Assert\Assert; final class Factory implements FactoryInterface diff --git a/src/Component/Symfony/ExpressionLanguage/ArgumentParser.php b/src/Component/Symfony/ExpressionLanguage/ArgumentParser.php new file mode 100644 index 000000000..9866e6a25 --- /dev/null +++ b/src/Component/Symfony/ExpressionLanguage/ArgumentParser.php @@ -0,0 +1,36 @@ +expressionLanguage->evaluate( + $expression, + array_merge( + $this->variablesCollection->getVariables(), + $variables, + ), + ); + } +} diff --git a/src/Component/Factory/ArgumentParserInterface.php b/src/Component/Symfony/ExpressionLanguage/ArgumentParserInterface.php similarity index 65% rename from src/Component/Factory/ArgumentParserInterface.php rename to src/Component/Symfony/ExpressionLanguage/ArgumentParserInterface.php index 4ba539812..5b1b7249d 100644 --- a/src/Component/Factory/ArgumentParserInterface.php +++ b/src/Component/Symfony/ExpressionLanguage/ArgumentParserInterface.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace Sylius\Component\Resource\Factory; +namespace Sylius\Component\Resource\Symfony\ExpressionLanguage; interface ArgumentParserInterface { - public function parseExpression(string $expression): mixed; + public function parseExpression(string $expression, array $variables = []): mixed; } diff --git a/src/Component/Symfony/ExpressionLanguage/RequestVariables.php b/src/Component/Symfony/ExpressionLanguage/RequestVariables.php new file mode 100644 index 000000000..f65eade76 --- /dev/null +++ b/src/Component/Symfony/ExpressionLanguage/RequestVariables.php @@ -0,0 +1,30 @@ + $this->requestStack->getCurrentRequest(), + ]; + } +} diff --git a/src/Component/Factory/ArgumentParser.php b/src/Component/Symfony/ExpressionLanguage/TokenVariables.php similarity index 53% rename from src/Component/Factory/ArgumentParser.php rename to src/Component/Symfony/ExpressionLanguage/TokenVariables.php index 139d967fe..d4672005d 100644 --- a/src/Component/Factory/ArgumentParser.php +++ b/src/Component/Symfony/ExpressionLanguage/TokenVariables.php @@ -11,24 +11,18 @@ declare(strict_types=1); -namespace Sylius\Component\Resource\Factory; +namespace Sylius\Component\Resource\Symfony\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\NullToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -final class ArgumentParser implements ArgumentParserInterface +final class TokenVariables implements VariablesInterface { - public function __construct( - private ExpressionLanguage $expressionLanguage, - private RequestStack $requestStack, - private ?TokenStorageInterface $tokenStorage = null, - ) { + public function __construct(private ?TokenStorageInterface $tokenStorage = null) + { } - public function parseExpression(string $expression): mixed + public function getVariables(): array { if (null === $this->tokenStorage) { throw new \LogicException('The "symfony/security-bundle" must be installed and configured to use the "token" & "user" attribute. Try running "composer require symfony/security-bundle"'); @@ -38,13 +32,7 @@ public function parseExpression(string $expression): mixed $token = new NullToken(); } - return $this->expressionLanguage->evaluate($expression, $this->getVariables($token)); - } - - private function getVariables(TokenInterface $token): array - { return [ - 'request' => $this->requestStack->getCurrentRequest(), 'token' => $token, 'user' => $token->getUser(), ]; diff --git a/src/Component/Symfony/ExpressionLanguage/VariablesCollection.php b/src/Component/Symfony/ExpressionLanguage/VariablesCollection.php new file mode 100644 index 000000000..2f0a9e5e2 --- /dev/null +++ b/src/Component/Symfony/ExpressionLanguage/VariablesCollection.php @@ -0,0 +1,36 @@ + $iterator */ + public function __construct(private iterable $iterator) + { + Assert::allIsInstanceOf($this->iterator, VariablesInterface::class); + } + + public function getVariables(): array + { + $variables = []; + + foreach ($this->iterator as $variable) { + $variables = array_merge($variables, $variable->getVariables()); + } + + return $variables; + } +} diff --git a/src/Component/Symfony/ExpressionLanguage/VariablesCollectionInterface.php b/src/Component/Symfony/ExpressionLanguage/VariablesCollectionInterface.php new file mode 100644 index 000000000..a18cff052 --- /dev/null +++ b/src/Component/Symfony/ExpressionLanguage/VariablesCollectionInterface.php @@ -0,0 +1,19 @@ +parseArgumentValues($operation->getRepositoryArguments() ?? []); if (\is_string($repository)) { $defaultMethod = $operation instanceof CollectionOperationInterface ? 'createPaginator' : 'findOneBy'; @@ -79,7 +82,10 @@ public function provide(Operation $operation, Context $context): object|iterable $reflector = CallableReflection::from($callable); } - $arguments = $this->argumentResolver->getArguments($request, $reflector); + if ([] === $arguments) { + $arguments = $this->argumentResolver->getArguments($request, $reflector); + } + $data = $repository(...$arguments); if ($data instanceof Pagerfanta) { @@ -89,4 +95,13 @@ public function provide(Operation $operation, Context $context): object|iterable return $data; } + + private function parseArgumentValues(array $arguments): array + { + foreach ($arguments as $key => $value) { + $arguments[$key] = $this->argumentParser->parseExpression($value); + } + + return $arguments; + } } diff --git a/src/Component/Symfony/Routing/ArgumentParser.php b/src/Component/Symfony/Routing/ArgumentParser.php deleted file mode 100644 index f75eed5fe..000000000 --- a/src/Component/Symfony/Routing/ArgumentParser.php +++ /dev/null @@ -1,44 +0,0 @@ -expressionLanguage->evaluate($expression, $this->getVariables($resource, $data)); - } - - private function getVariables(Resource $resource, mixed $data): array - { - $variables = [ - 'resource' => $data, - ]; - - $name = $resource->getName(); - - if (null !== $name) { - $variables[$name] = $data; - } - - return $variables; - } -} diff --git a/src/Component/Symfony/Routing/RedirectHandler.php b/src/Component/Symfony/Routing/RedirectHandler.php index 6eaea99a4..410207b7f 100644 --- a/src/Component/Symfony/Routing/RedirectHandler.php +++ b/src/Component/Symfony/Routing/RedirectHandler.php @@ -16,6 +16,7 @@ use Sylius\Component\Resource\Metadata\DeleteOperationInterface; use Sylius\Component\Resource\Metadata\HttpOperation; use Sylius\Component\Resource\Metadata\Resource; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -25,7 +26,7 @@ final class RedirectHandler { public function __construct( private RouterInterface $router, - private ArgumentParser $routingArgumentParser, + private ArgumentParserInterface $argumentParser, ) { } @@ -76,7 +77,14 @@ private function parseResourceValues(Resource $resource, array $parameters, mixe } } - $parameters[$key] = $this->routingArgumentParser->parseExpression($value, $resource, $data); + $variables = ['resource' => $data]; + $resourceName = $resource->getName(); + + if (null !== $resourceName) { + $variables[$resourceName] = $data; + } + + $parameters[$key] = $this->argumentParser->parseExpression($value, $variables); } return $parameters; diff --git a/src/Component/Tests/Symfony/ExpressionLanguage/ArgumentParserTest.php b/src/Component/Tests/Symfony/ExpressionLanguage/ArgumentParserTest.php new file mode 100644 index 000000000..aa37c4b32 --- /dev/null +++ b/src/Component/Tests/Symfony/ExpressionLanguage/ArgumentParserTest.php @@ -0,0 +1,60 @@ +get('sylius.expression_language.argument_parser.factory'); + + $this->assertInstanceOf(ArgumentParserInterface::class, $argumentParser); + $this->assertTrue($argumentParser->parseExpression('token.getUser() === null')); + $this->assertTrue($argumentParser->parseExpression('user === null')); + } + + public function testRepositoryArgumentParser(): void + { + self::bootKernel(); + + $container = static::getContainer(); + + /** @var ArgumentParserInterface $argumentParser */ + $argumentParser = $container->get('sylius.expression_language.argument_parser.repository'); + + $this->assertInstanceOf(ArgumentParserInterface::class, $argumentParser); + $this->assertTrue($argumentParser->parseExpression('token.getUser() === null')); + $this->assertTrue($argumentParser->parseExpression('user === null')); + } + + public function testRoutingArgumentParser(): void + { + self::bootKernel(); + + $container = static::getContainer(); + + /** @var ArgumentParserInterface $argumentParser */ + $argumentParser = $container->get('sylius.expression_language.argument_parser.routing'); + + $this->assertInstanceOf(ArgumentParserInterface::class, $argumentParser); + } +} diff --git a/src/Component/spec/Factory/ArgumentParserSpec.php b/src/Component/spec/Factory/ArgumentParserSpec.php deleted file mode 100644 index 9d2ea1a54..000000000 --- a/src/Component/spec/Factory/ArgumentParserSpec.php +++ /dev/null @@ -1,115 +0,0 @@ -beConstructedWith( - new ExpressionLanguage(), - $requestStack, - $tokenStorage, - ); - } - - function it_is_initializable(): void - { - $this->shouldHaveType(ArgumentParser::class); - } - - function it_parses_request_variable( - TokenStorageInterface $tokenStorage, - TokenInterface $token, - RequestStack $requestStack, - Request $request, - ): void { - $requestStack->getCurrentRequest()->willReturn($request); - - $request->attributes = new ParameterBag(['id' => '51353e91-5295-4876-a994-cae4b3ff3a7c']); - - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn(null); - - $this->parseExpression("request.attributes.get('id')")->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - } - - function it_parses_token_variable( - TokenStorageInterface $tokenStorage, - TokenInterface $token, - UserInterface $user, - ): void { - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn($user); - - $token->getUserIdentifier()->willReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - - $this->parseExpression('token.getUserIdentifier()')->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - } - - function it_parses_user_variable( - TokenStorageInterface $tokenStorage, - TokenInterface $token, - UserInterface $user, - ): void { - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn($user); - - $user->getUserIdentifier()->willReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - - $this->parseExpression('user.getUserIdentifier()')->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - } - - function its_user_variable_can_be_null( - TokenStorageInterface $tokenStorage, - TokenInterface $token, - ): void { - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn(null); - - $this->parseExpression('user === null')->shouldReturn(true); - } - - function it_throws_an_exception_when_token_storage_is_not_available( - RequestStack $requestStack, - TokenStorageInterface $tokenStorage, - TokenInterface $token, - ): void { - $this->beConstructedWith( - new ExpressionLanguage(), - $requestStack, - null, - ); - - $this->shouldThrow(new \LogicException('The "symfony/security-bundle" must be installed and configured to use the "token" & "user" attribute. Try running "composer require symfony/security-bundle"')) - ->during('parseExpression', ['']) - ; - } -} diff --git a/src/Component/spec/State/FactorySpec.php b/src/Component/spec/State/FactorySpec.php index ca064ffda..ff495815f 100644 --- a/src/Component/spec/State/FactorySpec.php +++ b/src/Component/spec/State/FactorySpec.php @@ -16,30 +16,18 @@ use PhpSpec\ObjectBehavior; use Psr\Container\ContainerInterface; use Sylius\Component\Resource\Context\Context; -use Sylius\Component\Resource\Factory\ArgumentParser; use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Metadata\Create; use Sylius\Component\Resource\State\Factory; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\User\UserInterface; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; final class FactorySpec extends ObjectBehavior { function let( ContainerInterface $locator, - RequestStack $requestStack, - TokenStorageInterface $tokenStorage, + ArgumentParserInterface $argumentParser, ): void { - $this->beConstructedWith($locator, new ArgumentParser( - new ExpressionLanguage(), - $requestStack->getWrappedObject(), - $tokenStorage->getWrappedObject(), - )); + $this->beConstructedWith($locator, $argumentParser); } function it_is_initializable(): void @@ -57,20 +45,14 @@ function it_calls_factory_from_operation_as_callable(): void } function it_calls_factory_with_arguments_from_operation_as_callable( - TokenStorageInterface $tokenStorage, - TokenInterface $token, - UserInterface $user, + ArgumentParserInterface $argumentParser, ): void { - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn($user); - - $user->getUserIdentifier()->willReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - $factory = [FactoryCallable::class, 'create']; $operation = new Create(factory: $factory, factoryArguments: ['userId' => 'user.getUserIdentifier()']); + $argumentParser->parseExpression('user.getUserIdentifier()')->willReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); + $result = $this->create($operation, new Context()); $result->shouldHaveType(\stdClass::class); $result->userId->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); @@ -91,68 +73,6 @@ function it_calls_factory_from_operation_as_string( $this->create($operation, new Context())->shouldReturn($data); } - function it_calls_factory_with_user_arguments_from_operation_as_string( - ContainerInterface $locator, - TokenStorageInterface $tokenStorage, - TokenInterface $token, - UserInterface $user, - ): void { - $factory = new ResourceFactory(); - - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn($user); - - $user->getUserIdentifier()->willReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - - $operation = new Create( - name: 'app_dummy_create', - factory: $factory::class, - factoryMethod: 'createForUser', - factoryArguments: ['userId' => 'user.getUserIdentifier()'], - ); - - $locator->has($factory::class)->willReturn(true); - $locator->get($factory::class)->willReturn($factory); - - $result = $this->create($operation, new Context()); - $result->shouldHaveType(\stdClass::class); - $result->userId->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - } - - function it_calls_factory_with_request_arguments_from_operation_as_string( - ContainerInterface $locator, - TokenStorageInterface $tokenStorage, - TokenInterface $token, - RequestStack $requestStack, - Request $request, - UserInterface $user, - ): void { - $factory = new ResourceFactory(); - - $requestStack->getCurrentRequest()->willReturn($request); - - $request->attributes = new ParameterBag(['id' => '51353e91-5295-4876-a994-cae4b3ff3a7c']); - - $tokenStorage->getToken()->willReturn($token); - - $token->getUser()->willReturn(null); - - $operation = new Create( - name: 'app_dummy_create', - factory: $factory::class, - factoryMethod: 'createForUser', - factoryArguments: ['userId' => "request.attributes.get('id')"], - ); - - $locator->has($factory::class)->willReturn(true); - $locator->get($factory::class)->willReturn($factory); - - $result = $this->create($operation, new Context()); - $result->shouldHaveType(\stdClass::class); - $result->userId->shouldReturn('51353e91-5295-4876-a994-cae4b3ff3a7c'); - } - function it_throws_an_exception_when_factory_is_not_found_on_locator( FactoryInterface $factory, ContainerInterface $locator, diff --git a/src/Component/spec/Symfony/ExpressionLanguage/ArgumentParserSpec.php b/src/Component/spec/Symfony/ExpressionLanguage/ArgumentParserSpec.php new file mode 100644 index 000000000..9787607ee --- /dev/null +++ b/src/Component/spec/Symfony/ExpressionLanguage/ArgumentParserSpec.php @@ -0,0 +1,46 @@ +beConstructedWith(new ExpressionLanguage(), $variablesCollection); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(ArgumentParser::class); + } + + function it_parses_expressions(VariablesCollectionInterface $variablesCollection): void + { + $variablesCollection->getVariables()->willReturn(['foo' => 'fighters']); + + $this->parseExpression('foo')->shouldReturn('fighters'); + } + + function it_merges_variables(VariablesCollectionInterface $variablesCollection): void + { + $variablesCollection->getVariables()->willReturn(['foo' => 'fighters']); + + $this->parseExpression('foo', ['foo' => 'bar'])->shouldReturn('bar'); + } +} diff --git a/src/Component/spec/Symfony/ExpressionLanguage/RequestVariablesSpec.php b/src/Component/spec/Symfony/ExpressionLanguage/RequestVariablesSpec.php new file mode 100644 index 000000000..3a768ee87 --- /dev/null +++ b/src/Component/spec/Symfony/ExpressionLanguage/RequestVariablesSpec.php @@ -0,0 +1,43 @@ +beConstructedWith($requestStack); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(RequestVariables::class); + } + + function it_returns_request_vars( + RequestStack $requestStack, + Request $request, + ): void { + $requestStack->getCurrentRequest()->willReturn($request); + + $this->getVariables()->shouldReturn([ + 'request' => $request, + ]); + } +} diff --git a/src/Component/spec/Symfony/ExpressionLanguage/TokenVariablesSpec.php b/src/Component/spec/Symfony/ExpressionLanguage/TokenVariablesSpec.php new file mode 100644 index 000000000..fde657120 --- /dev/null +++ b/src/Component/spec/Symfony/ExpressionLanguage/TokenVariablesSpec.php @@ -0,0 +1,87 @@ +beConstructedWith($tokenStorage); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(TokenVariables::class); + } + + function it_returns_token_and_user_vars( + TokenStorageInterface $tokenStorage, + TokenInterface $token, + UserInterface $user, + ): void { + $tokenStorage->getToken()->willReturn($token); + + $token->getUser()->willReturn($user); + + $this->getVariables()->shouldReturn([ + 'token' => $token, + 'user' => $user, + ]); + } + + function it_returns_a_null_token_if_there_is_no_token_on_storage( + TokenStorageInterface $tokenStorage, + TokenInterface $token, + ): void { + $tokenStorage->getToken()->willReturn(null); + + $token->getUser()->willReturn(null); + + $this->getVariables()['token']->shouldHaveType(NullToken::class); + } + + function it_can_return_null_as_user( + TokenStorageInterface $tokenStorage, + TokenInterface $token, + UserInterface $user, + ): void { + $tokenStorage->getToken()->willReturn($token); + + $token->getUser()->willReturn(null); + + $this->getVariables()->shouldReturn([ + 'token' => $token, + 'user' => null, + ]); + } + + function it_throws_an_exception_when_there_is_no_token_storage( + TokenStorageInterface $tokenStorage, + TokenInterface $token, + UserInterface $user, + ): void { + $this->beConstructedWith(null); + + $this->shouldThrow(new \LogicException('The "symfony/security-bundle" must be installed and configured to use the "token" & "user" attribute. Try running "composer require symfony/security-bundle"')) + ->during('getVariables') + ; + } +} diff --git a/src/Component/spec/Symfony/ExpressionLanguage/VariablesCollectionSpec.php b/src/Component/spec/Symfony/ExpressionLanguage/VariablesCollectionSpec.php new file mode 100644 index 000000000..fc3b7d618 --- /dev/null +++ b/src/Component/spec/Symfony/ExpressionLanguage/VariablesCollectionSpec.php @@ -0,0 +1,47 @@ +beConstructedWith([$firstVariables->getWrappedObject(), $secondVariables->getWrappedObject()]); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(VariablesCollection::class); + } + + function it_merges_variables( + VariablesInterface $firstVariables, + VariablesInterface $secondVariables, + ): void { + $firstVariables->getVariables()->willReturn(['foo' => 'bar', 'user' => '123']); + $secondVariables->getVariables()->willReturn(['foo' => 'fighters', 'value' => 'xyz']); + + $this->getVariables()->shouldReturn([ + 'foo' => 'fighters', + 'user' => '123', + 'value' => 'xyz', + ]); + } +} diff --git a/src/Component/spec/Symfony/Request/State/ProviderSpec.php b/src/Component/spec/Symfony/Request/State/ProviderSpec.php index 66d0676e8..9dc900cdd 100644 --- a/src/Component/spec/Symfony/Request/State/ProviderSpec.php +++ b/src/Component/spec/Symfony/Request/State/ProviderSpec.php @@ -21,6 +21,7 @@ use Sylius\Component\Resource\Metadata\Index; use Sylius\Component\Resource\Metadata\Operation; use Sylius\Component\Resource\Repository\RepositoryInterface; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Sylius\Component\Resource\Symfony\Request\RepositoryArgumentResolver; use Sylius\Component\Resource\Symfony\Request\State\Provider; use Sylius\Component\Resource\Tests\Dummy\RepositoryWithCallables; @@ -30,9 +31,9 @@ final class ProviderSpec extends ObjectBehavior { - function let(ContainerInterface $locator): void + function let(ContainerInterface $locator, ArgumentParserInterface $argumentParser): void { - $this->beConstructedWith($locator, new RepositoryArgumentResolver()); + $this->beConstructedWith($locator, new RepositoryArgumentResolver(), $argumentParser); } function it_is_initializable(): void @@ -45,6 +46,8 @@ function it_calls_repository_as_callable( Request $request, ): void { $operation->getRepository()->willReturn([RepositoryWithCallables::class, 'find']); + $operation->getRepositoryArguments()->willReturn(null); + $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id']]); $request->query = new InputBag([]); $request->request = new ParameterBag(); @@ -63,6 +66,7 @@ function it_calls_repository_as_string( ): void { $operation->getRepository()->willReturn('App\Repository'); $operation->getRepositoryMethod()->willReturn(null); + $operation->getRepositoryArguments()->willReturn(null); $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); $request->query = new InputBag([]); @@ -131,6 +135,7 @@ function it_calls_repository_as_string_with_specific_repository_method( ): void { $operation->getRepository()->willReturn('App\Repository'); $operation->getRepositoryMethod()->willReturn('find'); + $operation->getRepositoryArguments()->willReturn(null); $request->attributes = new ParameterBag(['_route_params' => ['id' => 'my_id', '_sylius' => ['resource' => 'app.dummy']]]); $request->query = new InputBag([]); @@ -144,4 +149,27 @@ function it_calls_repository_as_string_with_specific_repository_method( $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); $response->shouldReturn($stdClass); } + + function it_calls_repository_as_string_with_specific_repository_method_an_arguments( + Operation $operation, + Request $request, + ContainerInterface $locator, + RepositoryInterface $repository, + ArgumentParserInterface $argumentParser, + \stdClass $stdClass, + ): void { + $operation->getRepository()->willReturn('App\Repository'); + $operation->getRepositoryMethod()->willReturn('find'); + $operation->getRepositoryArguments()->willReturn(['id' => "request.attributes.get('id')"]); + + $argumentParser->parseExpression("request.attributes.get('id')")->willReturn('my_id'); + + $locator->has('App\Repository')->willReturn(true); + $locator->get('App\Repository')->willReturn($repository); + + $repository->find('my_id')->willReturn($stdClass); + + $response = $this->provide($operation, new Context(new RequestOption($request->getWrappedObject()))); + $response->shouldReturn($stdClass); + } } diff --git a/src/Component/spec/Symfony/Request/State/TwigResponderSpec.php b/src/Component/spec/Symfony/Request/State/TwigResponderSpec.php index e2700a418..ce7225b35 100644 --- a/src/Component/spec/Symfony/Request/State/TwigResponderSpec.php +++ b/src/Component/spec/Symfony/Request/State/TwigResponderSpec.php @@ -20,11 +20,10 @@ use Sylius\Component\Resource\Metadata\Index; use Sylius\Component\Resource\Metadata\Resource; use Sylius\Component\Resource\Metadata\Show; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Sylius\Component\Resource\Symfony\Request\State\TwigResponder; -use Sylius\Component\Resource\Symfony\Routing\ArgumentParser; use Sylius\Component\Resource\Symfony\Routing\RedirectHandler; use Sylius\Component\Resource\Twig\Context\Factory\ContextFactoryInterface; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; @@ -33,9 +32,13 @@ final class TwigResponderSpec extends ObjectBehavior { - function let(Environment $twig, RouterInterface $router, ContextFactoryInterface $contextFactory): void - { - $this->beConstructedWith(new RedirectHandler($router->getWrappedObject(), new ArgumentParser(new ExpressionLanguage())), $contextFactory, $twig); + function let( + Environment $twig, + RouterInterface $router, + ContextFactoryInterface $contextFactory, + ArgumentParserInterface $argumentParser, + ): void { + $this->beConstructedWith(new RedirectHandler($router->getWrappedObject(), $argumentParser->getWrappedObject()), $contextFactory, $twig); } function it_is_initializable(): void @@ -100,6 +103,7 @@ function it_redirect_to_route_after_creation( Request $request, ParameterBag $attributes, RouterInterface $router, + ArgumentParserInterface $argumentParser, ): void { $data->id = 'xyz'; $request->attributes = $attributes; @@ -109,6 +113,8 @@ function it_redirect_to_route_after_creation( $resource = new Resource(alias: 'app.book', pluralName: 'books'); $operation = (new Create(redirectToRoute: 'app_dummy_index'))->withResource($resource); + $argumentParser->parseExpression('resource.id', ['resource' => $data])->willReturn('xyz'); + $router->generate('app_dummy_index', ['id' => 'xyz'])->willReturn('/dummies')->shouldBeCalled(); $response = $this->respond($data, $operation, new Context(new RequestOption($request->getWrappedObject()))); diff --git a/src/Component/spec/Symfony/Routing/ArgumentParserSpec.php b/src/Component/spec/Symfony/Routing/ArgumentParserSpec.php deleted file mode 100644 index 3bb7a525b..000000000 --- a/src/Component/spec/Symfony/Routing/ArgumentParserSpec.php +++ /dev/null @@ -1,70 +0,0 @@ -beConstructedWith(new ExpressionLanguage()); - } - - function it_is_initializable(): void - { - $this->shouldHaveType(ArgumentParser::class); - } - - function it_parses_resource_argument_with_public_property( - \stdClass $data, - ): void { - $data->code = 'xyz'; - $resource = new Resource(alias: 'app.book'); - - $this->parseExpression('resource.code', $resource, $data)->shouldReturn('xyz'); - } - - function it_parses_resource_argument_via_a_getter(): void - { - $data = new BoardGame('uid'); - $resource = new Resource(alias: 'app.board_game'); - - $this->parseExpression('resource.id()', $resource, $data)->shouldReturn('uid'); - } - - function it_parses_resource_argument_with_resource_name( - \stdClass $data, - ): void { - $data->code = 'xyz'; - $resource = new Resource(alias: 'app.book', name: 'book'); - - $this->parseExpression('book.code', $resource, $data)->shouldReturn('xyz'); - } -} - -final class BoardGame -{ - public function __construct(private string $id) - { - } - - public function id(): string - { - return $this->id; - } -} diff --git a/src/Component/spec/Symfony/Routing/RedirectHandlerSpec.php b/src/Component/spec/Symfony/Routing/RedirectHandlerSpec.php index 9fb9a8c0d..010d2aa0d 100644 --- a/src/Component/spec/Symfony/Routing/RedirectHandlerSpec.php +++ b/src/Component/spec/Symfony/Routing/RedirectHandlerSpec.php @@ -18,19 +18,16 @@ use Sylius\Component\Resource\Metadata\Create; use Sylius\Component\Resource\Metadata\Delete; use Sylius\Component\Resource\Metadata\Resource; -use Sylius\Component\Resource\Symfony\Routing\ArgumentParser; +use Sylius\Component\Resource\Symfony\ExpressionLanguage\ArgumentParserInterface; use Sylius\Component\Resource\Symfony\Routing\RedirectHandler; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RouterInterface; final class RedirectHandlerSpec extends ObjectBehavior { - function let(RouterInterface $router): void + function let(RouterInterface $router, ArgumentParserInterface $argumentParser): void { - $this->beConstructedWith($router, new ArgumentParser( - new ExpressionLanguage(), - )); + $this->beConstructedWith($router, $argumentParser); } function it_is_initializable(): void @@ -72,7 +69,7 @@ function it_redirects_to_resource_with_id_via_property_access( Request $request, RouterInterface $router, ): void { - $data = new BoardGame('uid'); + $data = new BoardGameResource('uid'); $operation = new Create(redirectToRoute: 'app_board_game_index'); $resource = new Resource(alias: 'app.board_game'); $operation = $operation->withResource($resource); @@ -100,12 +97,15 @@ function it_redirects_to_resource_with_custom_arguments( function it_redirects_to_resource_with_id_via_the_getter( Request $request, RouterInterface $router, + ArgumentParserInterface $argumentParser, ): void { $data = new BoardGameResource('uid'); $operation = new Create(redirectToRoute: 'app_board_game_index', redirectArguments: ['id' => 'resource.id()']); $resource = new Resource(alias: 'app.board_game'); $operation = $operation->withResource($resource); + $argumentParser->parseExpression('resource.id()', ['resource' => $data])->willReturn('uid'); + $router->generate('app_board_game_index', ['id' => 'uid'])->willReturn('/board-games')->shouldBeCalled(); $this->redirectToResource($data, $operation, $request);