diff --git a/composer.json b/composer.json index 1ffc944596f..57fbdd50b49 100644 --- a/composer.json +++ b/composer.json @@ -134,6 +134,7 @@ "symfony/twig-bundle": "^5.4.21 || ^6.0", "symfony/validator": "^5.4.21 || ^6.0", "symfony/webpack-encore-bundle": "^1.15", + "symfony/workflow": "^5.4.21 || ^6.0", "symfony/yaml": "^5.4.21 || ^6.0", "twig/intl-extra": "^2.12 || ^3.4", "twig/twig": "^2.12 || ^3.3", diff --git a/config/packages/workflow.yaml b/config/packages/workflow.yaml new file mode 100644 index 00000000000..2a716ff0a1b --- /dev/null +++ b/config/packages/workflow.yaml @@ -0,0 +1,2 @@ +framework: + workflows: ~ diff --git a/psalm.xml b/psalm.xml index ecbe40dc73f..daca11c0e18 100644 --- a/psalm.xml +++ b/psalm.xml @@ -98,6 +98,7 @@ + diff --git a/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml b/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml index dca50202dc8..dcdbff194e8 100644 --- a/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml +++ b/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml @@ -15,6 +15,7 @@ framework: test: ~ mailer: dsn: 'null://null' + workflows: ~ security: firewalls: diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml index dbf7252c6ee..016b52f50f3 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml @@ -39,6 +39,7 @@ framework: paths: ['%kernel.project_dir%/config/serialization'] mailer: dsn: 'null://null' + workflows: ~ doctrine_migrations: storage: diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php index 06dc91b8346..7fe6d7db1c8 100644 --- a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php @@ -98,6 +98,16 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('state_machine') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('default_adapter')->defaultValue('winzou_state_machine')->end() + ->arrayNode('graphs_to_adapters_mapping') + ->useAttributeAsKey('graph_name') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() ->end() ; diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php index a38be15f801..3f01740907d 100644 --- a/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php @@ -72,6 +72,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('sylius_core.order_by_identifier', $config['order_by_identifier']); $container->setParameter('sylius_core.catalog_promotions.batch_size', $config['catalog_promotions']['batch_size']); $container->setParameter('sylius_core.price_history.batch_size', $config['price_history']['batch_size']); + $container->setParameter('sylius_core.state_machine.default_adapter', $config['state_machine']['default_adapter']); + $container->setParameter('sylius_core.state_machine.graphs_to_adapters_mapping', $config['state_machine']['graphs_to_adapters_mapping']); /** @var string $env */ $env = $container->getParameter('kernel.environment'); diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml index fb40a328529..a1b784e3895 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml @@ -37,6 +37,7 @@ + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_machines.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_machines.xml new file mode 100644 index 00000000000..4a48356fd53 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_machines.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + %sylius_core.state_machine.default_adapter% + %sylius_core.state_machine.graphs_to_adapters_mapping% + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/CompositeStateMachine.php b/src/Sylius/Bundle/CoreBundle/StateMachine/CompositeStateMachine.php new file mode 100644 index 00000000000..2771e8dd7e2 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/CompositeStateMachine.php @@ -0,0 +1,74 @@ + */ + private array $stateMachineAdapters; + + /** + * @param iterable $stateMachineAdapters + * @param array $graphsToAdaptersMapping + */ + public function __construct( + iterable $stateMachineAdapters, + private string $defaultAdapter, + private array $graphsToAdaptersMapping, + ) { + Assert::notEmpty($stateMachineAdapters, 'At least one state machine adapter should be provided.'); + Assert::allIsInstanceOf( + $stateMachineAdapters, + StateMachineInterface::class, + sprintf('All state machine adapters should implement the "%s" interface.', StateMachineInterface::class), + ); + $this->stateMachineAdapters = $stateMachineAdapters instanceof Traversable ? iterator_to_array($stateMachineAdapters) : $stateMachineAdapters; + } + + /** + * @throws \Exception + */ + public function can(object $subject, string $graphName, string $transition): bool + { + return $this->getStateMachineAdapter($graphName)->can($subject, $graphName, $transition); + } + + /** + * @throws \Exception + */ + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + $this->getStateMachineAdapter($graphName)->apply($subject, $graphName, $transition, $context); + } + + /** + * @throws \Exception + */ + public function getEnabledTransitions(object $subject, string $graphName): array + { + return $this->getStateMachineAdapter($graphName)->getEnabledTransitions($subject, $graphName); + } + + private function getStateMachineAdapter(string $graphName): StateMachineInterface + { + if (isset($this->graphsToAdaptersMapping[$graphName])) { + return $this->stateMachineAdapters[$this->graphsToAdaptersMapping[$graphName]]; + } + + return $this->stateMachineAdapters[$this->defaultAdapter]; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/Exception/StateMachineExecutionException.php b/src/Sylius/Bundle/CoreBundle/StateMachine/Exception/StateMachineExecutionException.php new file mode 100644 index 00000000000..d30bbf21d39 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/Exception/StateMachineExecutionException.php @@ -0,0 +1,18 @@ + $context + * + * @throws StateMachineExecutionException + */ + public function apply(object $subject, string $graphName, string $transition, array $context = []): void; + + /** + * @throws StateMachineExecutionException + * + * @return array + */ + public function getEnabledTransitions(object $subject, string $graphName): array; +} diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/SymfonyWorkflowAdapter.php b/src/Sylius/Bundle/CoreBundle/StateMachine/SymfonyWorkflowAdapter.php new file mode 100644 index 00000000000..6b020d7c53e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/SymfonyWorkflowAdapter.php @@ -0,0 +1,64 @@ +symfonyWorkflowRegistry->get($subject, $graphName)->can($subject, $transition); + } catch (InvalidArgumentException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + try { + $this->symfonyWorkflowRegistry->get($subject, $graphName)->apply($subject, $transition, $context); + } catch (InvalidArgumentException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function getEnabledTransitions(object $subject, string $graphName): array + { + try { + $enabledTransitions = $this->symfonyWorkflowRegistry->get($subject, $graphName)->getEnabledTransitions($subject); + } catch (InvalidArgumentException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + + return array_map( + function (SymfonyWorkflowTransition $transition): TransitionInterface { + return new Transition( + $transition->getName(), + $transition->getFroms(), + $transition->getTos(), + ); + }, + $enabledTransitions, + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/Transition.php b/src/Sylius/Bundle/CoreBundle/StateMachine/Transition.php new file mode 100644 index 00000000000..5f007c3005f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/Transition.php @@ -0,0 +1,43 @@ +|null $froms + * @param array|null $tos + */ + public function __construct( + private string $name, + private ?array $froms, + private ?array $tos, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function getFroms(): ?array + { + return $this->froms; + } + + public function getTos(): ?array + { + return $this->tos; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/TransitionInterface.php b/src/Sylius/Bundle/CoreBundle/StateMachine/TransitionInterface.php new file mode 100644 index 00000000000..ddfe68f69c0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/TransitionInterface.php @@ -0,0 +1,29 @@ +|null + */ + public function getFroms(): ?array; + + /** + * @return array|null + */ + public function getTos(): ?array; +} diff --git a/src/Sylius/Bundle/CoreBundle/StateMachine/WinzouStateMachineAdapter.php b/src/Sylius/Bundle/CoreBundle/StateMachine/WinzouStateMachineAdapter.php new file mode 100644 index 00000000000..54589bdca2c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/StateMachine/WinzouStateMachineAdapter.php @@ -0,0 +1,62 @@ +getStateMachine($subject, $graphName)->can($transition); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + try { + $this->getStateMachine($subject, $graphName)->apply($transition); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function getEnabledTransitions(object $subject, string $graphName): array + { + try { + $transitions = $this->getStateMachine($subject, $graphName)->getPossibleTransitions(); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + + return array_map( + fn (string $transition) => new Transition($transition, null, null), + $transitions, + ); + } + + private function getStateMachine(object $subject, string $graphName): \SM\StateMachine\StateMachineInterface + { + return $this->winzouStateMachineFactory->get($subject, $graphName); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php index 2a42d1a323d..84fc06b2d9a 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -72,6 +72,54 @@ public function it_allows_to_disable_order_by_identifier(): void ); } + /** @test */ + public function it_allows_to_configure_a_default_state_machine_adapter(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'state_machine' => [ + 'default_adapter' => 'symfony_workflow', + ], + ], + ], + [ + 'state_machine' => [ + 'default_adapter' => 'symfony_workflow', + 'graphs_to_adapters_mapping' => [], + ], + ], + 'state_machine', + ); + } + + /** @test */ + public function it_allows_to_configure_the_state_machines_adapters_mapping(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'state_machine' => [ + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ], + ], + [ + 'state_machine' => [ + 'default_adapter' => 'winzou_state_machine', + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ], + 'state_machine', + ); + } + /** @test */ public function it_throws_an_exception_if_value_other_then_integer_is_declared_as_batch_size(): void { diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php new file mode 100644 index 00000000000..b2888e855f0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php @@ -0,0 +1,47 @@ +getStateMachine(); + + $subject = new BlogPost(); + + $this->assertTrue($stateMachine->can($subject, 'app_blog_post', 'publish')); + } + + /** @test */ + public function it_calls_a_method_on_a_configured_adapter_for_a_given_graph(): void + { + $stateMachine = $this->getStateMachine(); + + $subject = new Comment(); + + $this->assertTrue($stateMachine->can($subject, 'app_comment', 'post')); + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius.state_machine.composite'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php b/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php new file mode 100644 index 00000000000..429cd2ab672 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php @@ -0,0 +1,36 @@ + + */ + public function getFunctions(): array + { + return [ + new TwigFunction('sylius_state_machine_can', [$this->stateMachine, 'can']), + new TwigFunction('sylius_state_machine_get_enabled_transitions', [$this->stateMachine, 'getEnabledTransitions']), + ]; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/composer.json b/src/Sylius/Bundle/CoreBundle/composer.json index eaa6257332c..e5480d6efaf 100644 --- a/src/Sylius/Bundle/CoreBundle/composer.json +++ b/src/Sylius/Bundle/CoreBundle/composer.json @@ -71,6 +71,7 @@ "symfony/messenger": "^5.4.21 || ^6.0", "symfony/templating": "^5.4.21 || ^6.0", "symfony/webpack-encore-bundle": "^1.15", + "symfony/workflow": "^5.4.21 || ^6.0", "winzou/state-machine-bundle": "^0.6" }, "require-dev": { diff --git a/src/Sylius/Bundle/CoreBundle/spec/StateMachine/CompositeStateMachineSpec.php b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/CompositeStateMachineSpec.php new file mode 100644 index 00000000000..defb811c469 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/CompositeStateMachineSpec.php @@ -0,0 +1,152 @@ +beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->can($subject, 'my_graph', 'my_transition')->willReturn(true); + $symfonyWorkflowAdapter->can($subject, 'my_graph', 'my_transition')->shouldNotBeCalled(); + + $this->can($subject, 'my_graph', 'my_transition')->shouldReturn(true); + } + + function it_invokes_the_can_method_on_a_state_machine_assigned_to_a_given_graph( + StateMachineInterface $winzouStateMachineAdapter, + StateMachineInterface $symfonyWorkflowAdapter, + ): void { + $this->beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [ + 'my_graph' => 'symfony_workflow', + ], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->can($subject, 'my_graph', 'my_transition')->shouldNotBeCalled(); + $symfonyWorkflowAdapter->can($subject, 'my_graph', 'my_transition')->willReturn(true); + + $this->can($subject, 'my_graph', 'my_transition')->shouldReturn(true); + } + + function it_invokes_the_apply_method_on_a_default_state_machine_adapter_when_no_state_machine_is_mapped_to_a_given_graph( + StateMachineInterface $winzouStateMachineAdapter, + StateMachineInterface $symfonyWorkflowAdapter, + ): void { + $this->beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->apply($subject, 'my_graph', 'my_transition', [])->shouldBeCalled(); + $symfonyWorkflowAdapter->apply($subject, 'my_graph', 'my_transition', [])->shouldNotBeCalled(); + + $this->apply($subject, 'my_graph', 'my_transition'); + } + + function it_invokes_the_apply_method_on_a_state_machine_assigned_to_a_given_graph( + StateMachineInterface $winzouStateMachineAdapter, + StateMachineInterface $symfonyWorkflowAdapter, + ): void { + $this->beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [ + 'my_graph' => 'symfony_workflow', + ], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->apply($subject, 'my_graph', 'my_transition', [])->shouldNotBeCalled(); + $symfonyWorkflowAdapter->apply($subject, 'my_graph', 'my_transition', [])->shouldBeCalled(); + + $this->apply($subject, 'my_graph', 'my_transition'); + } + + function it_invokes_the_get_enabled_transitions_method_on_a_default_state_machine_adapter_when_no_state_machine_is_mapped_to_a_given_graph( + StateMachineInterface $winzouStateMachineAdapter, + StateMachineInterface $symfonyWorkflowAdapter, + ): void { + $this->beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->getEnabledTransitions($subject, 'my_graph')->shouldBeCalled()->willReturn([]); + $symfonyWorkflowAdapter->getEnabledTransitions($subject, 'my_graph')->shouldNotBeCalled(); + + $this->getEnabledTransitions($subject, 'my_graph')->shouldReturn([]); + } + + function it_invokes_the_get_enabled_transitions_method_on_a_state_machine_assigned_to_a_given_graph( + StateMachineInterface $winzouStateMachineAdapter, + StateMachineInterface $symfonyWorkflowAdapter, + ): void { + $this->beConstructedWith( + [ + 'winzou_state_machine' => $winzouStateMachineAdapter, + 'symfony_workflow' => $symfonyWorkflowAdapter, + ], + 'winzou_state_machine', + [ + 'my_graph' => 'symfony_workflow', + ], + ); + + $subject = new \stdClass(); + + $winzouStateMachineAdapter->getEnabledTransitions($subject, 'my_graph')->shouldNotBeCalled(); + $symfonyWorkflowAdapter->getEnabledTransitions($subject, 'my_graph')->shouldBeCalled()->willReturn([]); + + $this->getEnabledTransitions($subject, 'my_graph')->shouldReturn([]); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/StateMachine/SymfonyWorkflowAdapterSpec.php b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/SymfonyWorkflowAdapterSpec.php new file mode 100644 index 00000000000..9ed55c7ee23 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/SymfonyWorkflowAdapterSpec.php @@ -0,0 +1,102 @@ +beConstructedWith($symfonyWorkflowRegistry); + } + + function it_returns_whether_a_transition_can_be_applied( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->can($subject, 'transition')->shouldBeCalled()->willReturn(true); + + $this->can($subject, 'some_workflow', 'transition')->shouldReturn(true); + } + + function it_translates_invalid_argument_exception_to_state_machine_execution_exception_on_the_can_method_call( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->can($subject, 'transition')->willThrow(new InvalidArgumentException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('can', [$subject, 'some_workflow', 'transition']); + } + + function it_applies_a_transition( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->apply($subject, 'transition', [])->shouldBeCalled(); + + $this->apply($subject, 'some_workflow', 'transition'); + } + + function it_translates_invalid_argument_exception_to_state_machine_execution_exception_on_the_apply_method_call( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->apply($subject, 'transition', [])->willThrow(new InvalidArgumentException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('apply', [$subject, 'some_workflow', 'transition']); + } + + function it_returns_enabled_transitions( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->getEnabledTransitions($subject)->shouldBeCalled()->willReturn([ + new SymfonyWorkflowTransition('transition', 'from', 'to'), + new SymfonyWorkflowTransition('transition2', ['from'], ['to']), + ]); + + $this->getEnabledTransitions($subject, 'some_workflow')->shouldBeLike([ + new Transition('transition', ['from'], ['to']), + new Transition('transition2', ['from'], ['to']), + ]); + } + + function it_translates_invalid_argument_exception_to_state_machine_execution_exception_on_the_get_enabled_transition_method_call( + Registry $symfonyWorkflowRegistry, + Workflow $someWorkflow, + \stdClass $subject, + ): void { + $symfonyWorkflowRegistry->get($subject, 'some_workflow')->willReturn($someWorkflow); + $someWorkflow->getEnabledTransitions($subject)->willThrow(new InvalidArgumentException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('getEnabledTransitions', [$subject, 'some_workflow']); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/StateMachine/WinzouStateMachineAdapterSpec.php b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/WinzouStateMachineAdapterSpec.php new file mode 100644 index 00000000000..07f4e0ab2a5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/StateMachine/WinzouStateMachineAdapterSpec.php @@ -0,0 +1,98 @@ +beConstructedWith($winzouStateMachineFactory); + } + + function it_returns_whether_a_transition_can_be_applied( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->can('transition')->shouldBeCalled()->willReturn(true); + + $this->can($subject, 'some_graph', 'transition')->shouldReturn(true); + } + + function it_translates_winzou_state_machines_exceptions_to_state_machine_execution_exception_on_the_can_method_call( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->can('transition')->willThrow(new SMException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('can', [$subject, 'some_graph', 'transition']); + } + + function it_applies_a_transition( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->apply('transition')->shouldBeCalled()->willReturn(true); + + $this->apply($subject, 'some_graph', 'transition'); + } + + function it_translates_winzou_state_machines_exceptions_to_state_machine_execution_exception_on_the_apply_method_call( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->apply('transition')->willThrow(new SMException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('apply', [$subject, 'some_graph', 'transition']); + } + + function it_returns_enabled_transitions( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->getPossibleTransitions()->shouldBeCalled()->willReturn(['transition', 'transition2']); + + $this->getEnabledTransitions($subject, 'some_graph')->shouldBeLike([ + new Transition('transition', null, null), + new Transition('transition2', null, null), + ]); + } + + function it_translates_winzou_state_machines_exceptions_to_state_machine_execution_exception_on_the_get_enabled_transition_method_call( + FactoryInterface $winzouStateMachineFactory, + StateMachineInterface $stateMachine, + \stdClass $subject, + ): void { + $winzouStateMachineFactory->get($subject, 'some_graph')->willReturn($stateMachine); + $stateMachine->getPossibleTransitions()->willThrow(new SMException('Invalid argument')); + + $this->shouldThrow(StateMachineExecutionException::class)->during('getEnabledTransitions', [$subject, 'some_graph']); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml index dca50202dc8..dcdbff194e8 100644 --- a/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml +++ b/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml @@ -15,6 +15,7 @@ framework: test: ~ mailer: dsn: 'null://null' + workflows: ~ security: firewalls: diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml new file mode 100644 index 00000000000..9af8b746cbb --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml @@ -0,0 +1,61 @@ +winzou_state_machine: + app_blog_post: + class: Sylius\Bundle\CoreBundle\Application\Model\BlogPost + property_path: state + graph: app_blog_post + state_machine_class: Sylius\Component\Resource\StateMachine\StateMachine + states: + new: ~ + published: ~ + unpublished: ~ + transitions: + post: + from: [new, unpublished] + to: published + unpost: + from: [published] + to: unpublished + app_comment: + class: Sylius\Bundle\CoreBundle\Application\Model\Comment + property_path: state + graph: app_comment + state_machine_class: Sylius\Component\Resource\StateMachine\StateMachine + states: + new: ~ + published: ~ + unpublished: ~ + transitions: + post: + from: [new, unpublished] + to: published + unpost: + from: [published] + to: unpublished + +framework: + workflows: + app_blog_post: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Bundle\CoreBundle\Application\Model\BlogPost + initial_marking: new + places: + - new + - published + - unpublished + transitions: + publish: + from: [new, unpublished] + to: published + unpublish: + from: published + to: unpublished + +sylius_core: + state_machine: + default_adapter: 'winzou_state_machine' + graphs_to_adapters_mapping: + app_blog_post: 'symfony_workflow' diff --git a/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php b/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php new file mode 100644 index 00000000000..8c3f6161ead --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php @@ -0,0 +1,32 @@ +state; + } + + public function setState(string $state): void + { + $this->state = $state; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php b/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php new file mode 100644 index 00000000000..c5bec03097a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php @@ -0,0 +1,32 @@ +state; + } + + public function setState(string $state): void + { + $this->state = $state; + } +} diff --git a/symfony.lock b/symfony.lock index bd5a114ac4e..d739fe27398 100644 --- a/symfony.lock +++ b/symfony.lock @@ -937,6 +937,18 @@ "webpack.config.js" ] }, + "symfony/workflow": { + "version": "5.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "3b2f8ca32a07fcb00f899649053943fa3d8bbfb6" + }, + "files": [ + "config/packages/workflow.yaml" + ] + }, "symfony/yaml": { "version": "v4.4.13" },