Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.6k
Description
Symfony version(s) affected
from 5.2 until 7.2
Description
When working with a workflow that uses two or more places at the same time the "workflow.[workflow name].entered.[place name]" events are being dispatched more than once (when the workflow actually entered a place). At the moment, this events are always dispatched (for all the places, that workflow is currently at) whenany place is entered, which in my opinion is wrong and causes problems in workflow executions because listeners for "workflow.[workflow name].entered.[place name]" events get executed more than once.
For example
The marking of workflow "reproduce_bug" just got to ['A' => 1, 'B' => 1] via a transition.
Then the events "workflow.reproduce_bug.entered.A" and "workflow.reproduce_bug.entered.B" where dispatched.
Everything is fine so far.
In the next trasition the marking gets changed from ['A' => 1, 'B' => 1] to ['A' => 1, 'D' => 1] via a transition.
Now the events "workflow.reproduce_bug.entered.D" and "workflow.reproduce_bug.entered.A" are being dispatched.
The event "workflow.reproduce_bug.entered.A" was dispatched again although the marking of the workflow was already at that place and it wasn't changed by the transition (as you can see in the example config below).
This behavior occures since version 5.2 of the Symfony Workflow Component up to current version 7.2.
How to reproduce
Ensure symfony/workflow component is installed
Could be any version >= 5.2 up to 7.2composer require symfony/workflow:7.2
Example workflow configuration
framework: workflows: reproduce_bug: type: workflow marking_store: type: method property: state supports: - App\WorkflowState initial_marking: initial_place places: - initial_place - A - B - C - D - E transitions: from_initial_place_to_a_and_b: from: initial_place to: - A - B from_a_to_c: from: A to: C from_b_to_d: from: B to: D
The App\WorkflowState entity
<?phpnamespace App;class WorkflowState{ protected $state; public function setState($state) { $this->state = $state; return $this; } public function getState() { return $this->state; }}
Simple symfony command to execute the workflows transition
<?phpnamespace App\Command;use Symfony\Component\Console\Command\Command;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Output\OutputInterface;use App\WorkflowState;use Symfony\Component\Workflow\Registry as WorkflowRegistry;class ReproduceWorkflowBugCommand extends Command{ /** * @return void */ protected function configure() { $this ->setName('ReproduceWorkflowBugCommand') ->setDescription('') ; } /** * @var Symfony\Component\Workflow\Registry */ protected $workflowRegistry; /** * @param Symfony\Component\Workflow\Registry $workflowRegistry */ public function __construct(WorkflowRegistry $workflowRegistry) { parent::__construct(); $this->workflowRegistry = $workflowRegistry; } /** * @param InputInterface $input * @param OutputInterface $output * * @return void */ protected function execute(InputInterface $input, OutputInterface $output): int { $workflowState = new WorkflowState(); $workflow = $this->workflowRegistry->get($workflowState, 'reproduce_bug'); $workflow->apply($workflowState, 'from_initial_place_to_a_and_b'); $workflow->apply($workflowState, 'from_b_to_d'); $workflow->apply($workflowState, 'from_a_to_c'); return 0; }}
A simple event subscriber for debugging
<?phpnamespace App\EventSubscriber;use Symfony\Component\EventDispatcher\EventSubscriberInterface;class TestEventSubscriber implements EventSubscriberInterface{ public static function getSubscribedEvents(): array { return [ 'workflow.reproduce_bug.entered.A' => 'triggerOnWorkflowReproduceBugEnteredAEvent', ]; } public function triggerOnWorkflowReproduceBugEnteredAEvent($event) { $transition = $event->getTransition(); $marking = $event->getMarking(); dump([ 'name' => $transition->getName(), 'froms' => $transition->getFroms(), 'tos' => $transition->getTos(), 'marking' => $marking->getPlaces(), ]); }}
When executing the commandbin/console ReproduceWorkflowBugCommand
Following output is shown
array:4 [ "name" => "from_initial_place_to_a_and_b" "froms" => array:1 [ 0 => "initial_place" ] "tos" => array:2 [ 0 => "A" 1 => "B" ] "marking" => array:2 [ "A" => 1 "B" => 1 ]]array:4 [ "name" => "from_b_to_d" "froms" => array:1 [ 0 => "B" ] "tos" => array:1 [ 0 => "D" ] "marking" => array:2 [ "A" => 1 "D" => 1 ]]
This output shows that the "workflow.reproduce_bug.entered.A" event was dispatched twice.
Once for transition "from_initial_place_to_a_and_b" which is expected and once for transition "from_b_to_d" which wasn't expected.
Possible Solution
The issue is caused within theentered
method of theSymfony\Component\Workflow\Workflow
class as it dispatches the "workflow.[workflow name].entered.[place name]" events for the current marking places of the workflow.
Code, wich causes the issuevendor/symfony/workflow/Workflow.php
private function entered(...){ ... foreach ($marking->getPlaces() as $placeName => $nbToken) { $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered.%s', $this->name, $placeName)); }}
Fix
Instead of dispatching the events for each place of the marking, dispatch the events only for the "tos" of the transition. It was actually done this way until version 5.1 of the symfony workflow component.
if (null !== $transition) { foreach ($transition->getTos() as $placeName) { $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered.%s', $this->name, $placeName)); }}
Additional Context
No response