/
githubmirror
/
symfony
Обзор
Документация
Войти
/
githubmirror
/
symfony
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
8.2
src/Symfony/Component/Workflow/Tests/WorkflowTest.php
1 056 строк
43 KB
Ousama Ben Younes
[Workflow] Permanently disable events via the "!event" syntax in events_to_dispatch
23 июн 2026, 09:52
23 июн 2026, 09:52
2bf50ce
Код
Авторство
О чём код?
<?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Workflow\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Workflow\Arc; use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Event\EnteredEvent; use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\Event\TransitionEvent; use Symfony\Component\Workflow\Exception\LogicException; use Symfony\Component\Workflow\Exception\NotEnabledTransitionException; use Symfony\Component\Workflow\Exception\UndefinedTransitionException; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; use Symfony\Component\Workflow\Transition; use Symfony\Component\Workflow\TransitionBlocker; use Symfony\Component\Workflow\Workflow; use Symfony\Component\Workflow\WorkflowEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class WorkflowTest extends TestCase { use WorkflowBuilderTrait; public function testGetMarkingWithEmptyDefinition() { $this->expectException(LogicException::class); $this->expectExceptionMessage('The Marking is empty and there is no initial place for workflow "unnamed".'); $subject = new Subject(); $workflow = new Workflow(new Definition([], []), new MethodMarkingStore()); $workflow->getMarking($subject); } public function testGetMarkingWithImpossiblePlace() { $this->expectException(LogicException::class); $this->expectExceptionMessage('Place "nope" is not valid for workflow "unnamed".'); $subject = new Subject(); $subject->setMarking(['nope' => 1]); $workflow = new Workflow(new Definition([], []), new MethodMarkingStore()); $workflow->getMarking($subject); } public function testGetMarkingWithEmptyInitialMarking() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->getMarking($subject); $this->assertInstanceOf(Marking::class, $marking); $this->assertTrue($marking->has('a')); $this->assertSame(['a' => 1], $subject->getMarking()); } public function testGetMarkingWithExistingMarking() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $subject->setMarking(['b' => 1, 'c' => 1]); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->getMarking($subject); $this->assertInstanceOf(Marking::class, $marking); $this->assertTrue($marking->has('b')); $this->assertTrue($marking->has('c')); } public function testCanWithUnexistingTransition() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $this->assertFalse($workflow->can($subject, 'foobar')); } public function testCan() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $this->assertTrue($workflow->can($subject, 't1')); $this->assertFalse($workflow->can($subject, 't2')); $subject->setMarking(['b' => 1]); $this->assertFalse($workflow->can($subject, 't1')); // In a workflow net, all "from" places should contain a token to enable // the transition. $this->assertFalse($workflow->can($subject, 't2')); $subject->setMarking(['b' => 1, 'c' => 1]); $this->assertFalse($workflow->can($subject, 't1')); $this->assertTrue($workflow->can($subject, 't2')); $subject->setMarking(['f' => 1]); $this->assertFalse($workflow->can($subject, 't5')); $this->assertTrue($workflow->can($subject, 't6')); } public function testCanWithGuard() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcher(); $eventDispatcher->addListener('workflow.workflow_name.guard.t1', static function (GuardEvent $event) { $event->setBlocked(true); }); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $this->assertFalse($workflow->can($subject, 't1')); } public function testCanDoesNotTriggerGuardEventsForNotEnabledTransitions() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $dispatchedEvents = []; $eventDispatcher = new EventDispatcher(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $workflow->apply($subject, 't1'); $workflow->apply($subject, 't2'); $eventDispatcher->addListener('workflow.workflow_name.guard.t3', static function () use (&$dispatchedEvents) { $dispatchedEvents[] = 'workflow_name.guard.t3'; }); $eventDispatcher->addListener('workflow.workflow_name.guard.t4', static function () use (&$dispatchedEvents) { $dispatchedEvents[] = 'workflow_name.guard.t4'; }); $workflow->can($subject, 't3'); $this->assertSame(['workflow_name.guard.t3'], $dispatchedEvents); } public function testCanWithSameNameTransition() { $definition = $this->createWorkflowWithSameNameTransition(); $workflow = new Workflow($definition, new MethodMarkingStore()); $subject = new Subject(); $this->assertTrue($workflow->can($subject, 'a_to_bc')); $this->assertFalse($workflow->can($subject, 'b_to_c')); $this->assertFalse($workflow->can($subject, 'to_a')); $subject->setMarking(['b' => 1]); $this->assertFalse($workflow->can($subject, 'a_to_bc')); $this->assertTrue($workflow->can($subject, 'b_to_c')); $this->assertTrue($workflow->can($subject, 'to_a')); } public function testBuildTransitionBlockerListReturnsUndefinedTransition() { $this->expectException(UndefinedTransitionException::class); $this->expectExceptionMessage('Transition "404 Not Found" is not defined for workflow "unnamed".'); $definition = $this->createSimpleWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $workflow->buildTransitionBlockerList($subject, '404 Not Found'); } public function testBuildTransitionBlockerList() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty()); $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty()); $subject->setMarking(['b' => 1]); $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty()); $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty()); $subject->setMarking(['b' => 1, 'c' => 1]); $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't1')->isEmpty()); $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't2')->isEmpty()); $subject->setMarking(['f' => 1]); $this->assertFalse($workflow->buildTransitionBlockerList($subject, 't5')->isEmpty()); $this->assertTrue($workflow->buildTransitionBlockerList($subject, 't6')->isEmpty()); } public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't2'); $this->assertCount(1, $transitionBlockerList); $blockers = iterator_to_array($transitionBlockerList); $this->assertSame('The marking does not enable the transition.', $blockers[0]->getMessage()); $this->assertSame('19beefc8-6b1e-4716-9d07-a39bd6d16e34', $blockers[0]->getCode()); } public function testBuildTransitionBlockerListReturnsReasonsProvidedInGuards() { $definition = $this->createSimpleWorkflowDefinition(); $subject = new Subject(); $dispatcher = new EventDispatcher(); $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher); $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) { $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 1', 'blocker_1')); $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 2', 'blocker_2')); }); $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) { $event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3')); }); $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) { $event->setBlocked(true); }); $dispatcher->addListener('workflow.guard', static function (GuardEvent $event) { $event->setBlocked(true, 'You should not pass !!'); }); $transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1'); $this->assertCount(5, $transitionBlockerList); $blockers = iterator_to_array($transitionBlockerList); $this->assertSame('Transition blocker 1', $blockers[0]->getMessage()); $this->assertSame('blocker_1', $blockers[0]->getCode()); $this->assertSame('Transition blocker 2', $blockers[1]->getMessage()); $this->assertSame('blocker_2', $blockers[1]->getCode()); $this->assertSame('Transition blocker 3', $blockers[2]->getMessage()); $this->assertSame('blocker_3', $blockers[2]->getCode()); $this->assertSame('The transition has been blocked by a guard (Symfony\Component\Workflow\Tests\WorkflowTest).', $blockers[3]->getMessage()); $this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[3]->getCode()); $this->assertSame('You should not pass !!', $blockers[4]->getMessage()); $this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[4]->getCode()); } public function testApplyWithNotExisingTransition() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $context = [ 'lorem' => 'ipsum', ]; try { $workflow->apply($subject, '404 Not Found', $context); $this->fail('Should throw an exception'); } catch (UndefinedTransitionException $e) { $this->assertSame('Transition "404 Not Found" is not defined for workflow "unnamed".', $e->getMessage()); $this->assertSame($e->getContext(), $context); } } public function testApplyWithNotEnabledTransition() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $context = [ 'lorem' => 'ipsum', ]; try { $workflow->apply($subject, 't2', $context); $this->fail('Should throw an exception'); } catch (NotEnabledTransitionException $e) { $this->assertSame('Cannot apply transition "t2" on workflow "unnamed".', $e->getMessage()); $this->assertCount(1, $e->getTransitionBlockerList()); $list = iterator_to_array($e->getTransitionBlockerList()); $this->assertSame('The marking does not enable the transition.', $list[0]->getMessage()); $this->assertSame($e->getWorkflow(), $workflow); $this->assertSame($e->getSubject(), $subject); $this->assertSame($e->getTransitionName(), 't2'); $this->assertSame($e->getContext(), $context); } } public function testApply() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->apply($subject, 't1'); $this->assertInstanceOf(Marking::class, $marking); $this->assertFalse($marking->has('a')); $this->assertTrue($marking->has('b')); $this->assertTrue($marking->has('c')); } public function testApplyWithSameNameTransition() { $subject = new Subject(); $definition = $this->createWorkflowWithSameNameTransition(); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->apply($subject, 'a_to_bc'); $this->assertPlaces([ 'b' => 1, 'c' => 1, ], $marking); $marking = $workflow->apply($subject, 'to_a'); // Two tokens in "a" $this->assertPlaces([ 'a' => 2, ], $marking); $workflow->apply($subject, 'a_to_bc'); $marking = $workflow->apply($subject, 'b_to_c'); $this->assertPlaces([ 'a' => 1, 'c' => 2, ], $marking); $marking = $workflow->apply($subject, 'to_a'); $this->assertPlaces([ 'a' => 2, 'c' => 1, ], $marking); } public function testApplyWithSameNameTransition2() { $subject = new Subject(); $subject->setMarking(['a' => 1, 'b' => 1]); $places = range('a', 'd'); $transitions = []; $transitions[] = new Transition('t', 'a', 'c'); $transitions[] = new Transition('t', 'b', 'd'); $definition = new Definition($places, $transitions); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->apply($subject, 't'); $this->assertFalse($marking->has('a')); $this->assertFalse($marking->has('b')); $this->assertTrue($marking->has('c')); $this->assertTrue($marking->has('d')); } public function testApplyWithSameNameTransition3() { $subject = new Subject(); $subject->setMarking(['a' => 1]); $places = range('a', 'd'); $transitions = []; $transitions[] = new Transition('t', 'a', 'b'); $transitions[] = new Transition('t', 'b', 'c'); $transitions[] = new Transition('t', 'c', 'd'); $definition = new Definition($places, $transitions); $workflow = new Workflow($definition, new MethodMarkingStore()); $marking = $workflow->apply($subject, 't'); // We want to make sure we do not end up in "d" $this->assertTrue($marking->has('b')); $this->assertFalse($marking->has('d')); } public function testApplyWithEventDispatcher() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $eventNameExpected = [ 'workflow.entered', 'workflow.workflow_name.entered', 'workflow.workflow_name.entered.a', 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.t1', 'workflow.leave', 'workflow.workflow_name.leave', 'workflow.workflow_name.leave.a', 'workflow.transition', 'workflow.workflow_name.transition', 'workflow.workflow_name.transition.t1', 'workflow.enter', 'workflow.workflow_name.enter', 'workflow.workflow_name.enter.b', 'workflow.workflow_name.enter.c', 'workflow.entered', 'workflow.workflow_name.entered', 'workflow.workflow_name.entered.b', 'workflow.workflow_name.entered.c', 'workflow.completed', 'workflow.workflow_name.completed', 'workflow.workflow_name.completed.t1', // Following events are fired because of announce() method 'workflow.announce', 'workflow.workflow_name.announce', 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.t2', 'workflow.workflow_name.announce.t2', ]; $workflow->apply($subject, 't1'); $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } public static function provideApplyWithEventDispatcherForAnnounceTests(): \Generator { yield [false, [Workflow::DISABLE_ANNOUNCE_EVENT => true]]; yield [true, [Workflow::DISABLE_ANNOUNCE_EVENT => false]]; yield [true, []]; } #[DataProvider('provideApplyWithEventDispatcherForAnnounceTests')] public function testApplyWithEventDispatcherForAnnounce(bool $fired, array $context) { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $workflow->apply($subject, 't1', $context); if ($fired) { $this->assertContains('workflow.workflow_name.announce', $eventDispatcher->dispatchedEvents); } else { $this->assertNotContains('workflow.workflow_name.announce', $eventDispatcher->dispatchedEvents); } } public function testApplyDispatchesWithDisableEventInContext() { $transitions[] = new Transition('a-b', 'a', 'b'); $transitions[] = new Transition('a-c', 'a', 'c'); $definition = new Definition(['a', 'b', 'c'], $transitions); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $eventNameExpected = [ 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.a-b', 'workflow.transition', 'workflow.workflow_name.transition', 'workflow.workflow_name.transition.a-b', ]; $workflow->apply($subject, 'a-b', [ Workflow::DISABLE_LEAVE_EVENT => true, Workflow::DISABLE_ENTER_EVENT => true, Workflow::DISABLE_ENTERED_EVENT => true, Workflow::DISABLE_COMPLETED_EVENT => true, Workflow::DISABLE_ANNOUNCE_EVENT => true, ]); $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } public function testApplyDispatchesNoEventsWhenSpecifiedByDefinition() { $transitions[] = new Transition('a-b', 'a', 'b'); $transitions[] = new Transition('a-c', 'a', 'c'); $definition = new Definition(['a', 'b', 'c'], $transitions); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name', []); $eventNameExpected = [ 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.a-b', ]; $workflow->apply($subject, 'a-b'); $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } public function testApplyDoesNotDispatchEventsBlockListedInEventsToDispatch() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); // `!workflow.announce` = fire every event except `workflow.announce`. Future // WorkflowEvents keep being dispatched without needing this list to be updated. $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name', ['!'.WorkflowEvents::ANNOUNCE]); $workflow->apply($subject, 't1'); $workflow->apply($subject, 't2'); // Sanity: non-block-listed events still fire (a literal-allow-list parser would // suppress everything and the next foreach would pass vacuously — assertNotEmpty // closes that loophole). $this->assertNotEmpty($eventDispatcher->dispatchedEvents, 'Non-block-listed WorkflowEvents must still fire.'); $this->assertContains('workflow.workflow_name.transition', $eventDispatcher->dispatchedEvents); foreach ($eventDispatcher->dispatchedEvents as $event) { $this->assertStringNotContainsString('announce', $event, \sprintf('Announce event "%s" must not be dispatched when block-listed via "!" in $eventsToDispatch.', $event)); } } public function testBlockListingGuardEventThrows() { $this->expectException(\Symfony\Component\Workflow\Exception\InvalidArgumentException::class); $this->expectExceptionMessage('The "workflow.guard" event cannot be disabled in $eventsToDispatch for workflow "workflow_name": it is always dispatched.'); new Workflow($this->createComplexWorkflowDefinition(), new MethodMarkingStore(), new EventDispatcherMock(), 'workflow_name', ['!'.WorkflowEvents::GUARD]); } public function testMixingAllowListAndBlockListInEventsToDispatchThrows() { $this->expectException(\Symfony\Component\Workflow\Exception\InvalidArgumentException::class); $this->expectExceptionMessage('Cannot mix allow-list and block-list entries in $eventsToDispatch for workflow "mixed": every entry must start with "!" (block-list mode) or none of them must (allow-list mode).'); new Workflow($this->createComplexWorkflowDefinition(), new MethodMarkingStore(), new EventDispatcherMock(), 'mixed', [WorkflowEvents::COMPLETED, '!'.WorkflowEvents::ANNOUNCE]); } public function testApplyOnlyDispatchesEventsThatHaveBeenSpecifiedByDefinition() { $transitions[] = new Transition('a-b', 'a', 'b'); $transitions[] = new Transition('a-c', 'a', 'c'); $definition = new Definition(['a', 'b', 'c'], $transitions); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name', [WorkflowEvents::COMPLETED]); $eventNameExpected = [ 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.a-b', 'workflow.completed', 'workflow.workflow_name.completed', 'workflow.workflow_name.completed.a-b', ]; $workflow->apply($subject, 'a-b'); $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } public function testApplyDoesNotTriggerExtraGuardWithEventDispatcher() { $transitions[] = new Transition('a-b', 'a', 'b'); $transitions[] = new Transition('a-c', 'a', 'c'); $definition = new Definition(['a', 'b', 'c'], $transitions); $subject = new Subject(); $eventDispatcher = new EventDispatcherMock(); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $eventNameExpected = [ 'workflow.entered', 'workflow.workflow_name.entered', 'workflow.workflow_name.entered.a', 'workflow.guard', 'workflow.workflow_name.guard', 'workflow.workflow_name.guard.a-b', 'workflow.leave', 'workflow.workflow_name.leave', 'workflow.workflow_name.leave.a', 'workflow.transition', 'workflow.workflow_name.transition', 'workflow.workflow_name.transition.a-b', 'workflow.enter', 'workflow.workflow_name.enter', 'workflow.workflow_name.enter.b', 'workflow.entered', 'workflow.workflow_name.entered', 'workflow.workflow_name.entered.b', 'workflow.completed', 'workflow.workflow_name.completed', 'workflow.workflow_name.completed.a-b', 'workflow.announce', 'workflow.workflow_name.announce', ]; $workflow->apply($subject, 'a-b'); $this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents); } public function testApplyWithContext() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcher(); $eventDispatcher->addListener('workflow.transition', static function (TransitionEvent $event) { $event->setContext(array_merge($event->getContext(), ['user' => 'admin'])); }); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher); $workflow->apply($subject, 't1', ['foo' => 'bar']); $this->assertSame(['foo' => 'bar', 'user' => 'admin'], $subject->getContext()); } public function testEventName() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $dispatcher = new EventDispatcher(); $name = 'workflow_name'; $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name); $assertWorkflowName = function (Event $event) use ($name) { $this->assertEquals($name, $event->getWorkflowName()); }; $eventNames = [ 'workflow.guard', 'workflow.leave', 'workflow.transition', 'workflow.enter', 'workflow.entered', 'workflow.announce', ]; foreach ($eventNames as $eventName) { $dispatcher->addListener($eventName, $assertWorkflowName); } $workflow->apply($subject, 't1'); } public function testEventContext() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $dispatcher = new EventDispatcher(); $name = 'workflow_name'; $context = ['context']; $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name); $assertWorkflowContext = function (Event $event) use ($context) { $this->assertEquals($context, $event->getContext()); }; $eventNames = [ 'workflow.leave', 'workflow.transition', 'workflow.enter', 'workflow.entered', 'workflow.announce', ]; foreach ($eventNames as $eventName) { $dispatcher->addListener($eventName, $assertWorkflowContext); } $marking = $workflow->apply($subject, 't1', $context); $this->assertInstanceOf(Marking::class, $marking); $this->assertSame($context, $marking->getContext()); } public function testEventContextUpdated() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $dispatcher = new EventDispatcher(); $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher); $dispatcher->addListener('workflow.transition', static function (TransitionEvent $event) { $event->setContext(['foo' => 'bar']); }); $marking = $workflow->apply($subject, 't1', ['initial']); $this->assertInstanceOf(Marking::class, $marking); $this->assertSame(['foo' => 'bar'], $marking->getContext()); } public function testEventDefaultInitialContext() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $dispatcher = new EventDispatcher(); $name = 'workflow_name'; $context = Workflow::DEFAULT_INITIAL_CONTEXT; $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name); $assertWorkflowContext = function (Event $event) use ($context) { $this->assertEquals($context, $event->getContext()); }; $eventNames = [ 'workflow.workflow_name.entered.a', ]; foreach ($eventNames as $eventName) { $dispatcher->addListener($eventName, $assertWorkflowContext); } $workflow->apply($subject, 't1'); } public function testEventWhenAlreadyInThisPlace() { // ┌──────┐ ┌──────────────────────┐ ┌───┐ ┌─────────────┐ ┌───┐ // │ init │ ──▶ │ from_init_to_a_and_b │ ──▶ │ B │ ──▶ │ from_b_to_c │ ──▶ │ C │ // └──────┘ └──────────────────────┘ └───┘ └─────────────┘ └───┘ // │ // │ // ▼ // ┌───────────────────────────────┐ // │ A │ // └───────────────────────────────┘ $definition = new Definition( ['init', 'A', 'B', 'C'], [ new Transition('from_init_to_a_and_b', 'init', ['A', 'B']), new Transition('from_b_to_c', 'B', 'C'), ], ); $subject = new Subject(); $dispatcher = new EventDispatcher(); $name = 'workflow_name'; $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name); $calls = []; $listener = static function (Event $event) use (&$calls) { $calls[] = $event; }; $dispatcher->addListener("workflow.$name.entered.A", $listener); $workflow->apply($subject, 'from_init_to_a_and_b'); $workflow->apply($subject, 'from_b_to_c'); $this->assertCount(1, $calls); $this->assertInstanceOf(EnteredEvent::class, $calls[0]); $this->assertSame('from_init_to_a_and_b', $calls[0]->getTransition()->getName()); } public function testMarkingStateOnApplyWithEventDispatcher() { $definition = new Definition(range('a', 'f'), [new Transition('t', range('a', 'c'), range('d', 'f'))]); $subject = new Subject(); $subject->setMarking(['a' => 1, 'b' => 1, 'c' => 1]); $dispatcher = new EventDispatcher(); $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, 'test'); $assertInitialState = function (Event $event) { $this->assertEquals(new Marking(['a' => 1, 'b' => 1, 'c' => 1]), $event->getMarking()); }; $assertTransitionState = function (Event $event) { $this->assertEquals(new Marking([]), $event->getMarking()); }; $dispatcher->addListener('workflow.leave', $assertInitialState); $dispatcher->addListener('workflow.test.leave', $assertInitialState); $dispatcher->addListener('workflow.test.leave.a', $assertInitialState); $dispatcher->addListener('workflow.test.leave.b', $assertInitialState); $dispatcher->addListener('workflow.test.leave.c', $assertInitialState); $dispatcher->addListener('workflow.transition', $assertTransitionState); $dispatcher->addListener('workflow.test.transition', $assertTransitionState); $dispatcher->addListener('workflow.test.transition.t', $assertTransitionState); $dispatcher->addListener('workflow.enter', $assertTransitionState); $dispatcher->addListener('workflow.test.enter', $assertTransitionState); $dispatcher->addListener('workflow.test.enter.d', $assertTransitionState); $dispatcher->addListener('workflow.test.enter.e', $assertTransitionState); $dispatcher->addListener('workflow.test.enter.f', $assertTransitionState); $workflow->apply($subject, 't'); } public function testGetEnabledTransitions() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $eventDispatcher = new EventDispatcher(); $eventDispatcher->addListener('workflow.workflow_name.guard.t1', static function (GuardEvent $event) { $event->setBlocked(true); }); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); $this->assertSame([], $workflow->getEnabledTransitions($subject)); $subject->setMarking(['d' => 1]); $transitions = $workflow->getEnabledTransitions($subject); $this->assertCount(2, $transitions); $this->assertSame('t3', $transitions[0]->getName()); $this->assertSame('t4', $transitions[1]->getName()); $subject->setMarking(['c' => 1, 'e' => 1]); $transitions = $workflow->getEnabledTransitions($subject); $this->assertCount(1, $transitions); $this->assertSame('t5', $transitions[0]->getName()); } public function testGetEnabledTransition() { $definition = $this->createComplexWorkflowDefinition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $subject->setMarking(['d' => 1]); $transition = $workflow->getEnabledTransition($subject, 't3'); $this->assertInstanceOf(Transition::class, $transition); $this->assertSame('t3', $transition->getName()); $transition = $workflow->getEnabledTransition($subject, 'does_not_exist'); $this->assertNull($transition); } public function testGetEnabledTransitionsWithSameNameTransition() { $definition = $this->createWorkflowWithSameNameTransition(); $subject = new Subject(); $workflow = new Workflow($definition, new MethodMarkingStore()); $transitions = $workflow->getEnabledTransitions($subject); $this->assertCount(1, $transitions); $this->assertSame('a_to_bc', $transitions[0]->getName()); $subject->setMarking(['b' => 1, 'c' => 1]); $transitions = $workflow->getEnabledTransitions($subject); $this->assertCount(3, $transitions); $this->assertSame('b_to_c', $transitions[0]->getName()); $this->assertSame('to_a', $transitions[1]->getName()); $this->assertSame('to_a', $transitions[2]->getName()); } #[TestWith(['back1'])] #[TestWith(['back2'])] public function testApplyWithSameNameBackTransition(string $transition) { $definition = $this->createWorkflowWithSameNameBackTransition(); $workflow = new Workflow($definition, new MethodMarkingStore()); $subject = new Subject(); $marking = $workflow->apply($subject, 'a_to_bc'); $this->assertPlaces([ 'b' => 1, 'c' => 1, ], $marking); $marking = $workflow->apply($subject, $transition); $this->assertPlaces([ 'a' => 1, 'b' => 1, ], $marking); $marking = $workflow->apply($subject, $transition); $this->assertPlaces([ 'a' => 2, ], $marking); $marking = $workflow->apply($subject, 'a_to_bc'); $this->assertPlaces([ 'a' => 1, 'b' => 1, 'c' => 1, ], $marking); $marking = $workflow->apply($subject, 'c_to_cb'); $this->assertPlaces([ 'a' => 1, 'b' => 2, 'c' => 1, ], $marking); $marking = $workflow->apply($subject, 'c_to_cb'); $this->assertPlaces([ 'a' => 1, 'b' => 3, 'c' => 1, ], $marking); } public function testWithArcAndWeight() { // ┌───────────────────┐ ┌─────────────┐ ┌─────────────┐ 4 // │ prepare_leg │ ──▶ │ build_leg │ ──▶ │ leg_created │ ───────────────────────────┐ // └───────────────────┘ └─────────────┘ └─────────────┘ │ // ▲ │ // │ 4 │ // │ ▼ // ┌──────┐ ┌───────────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────┐ ┌──────────┐ // │ init │ ──▶ │ start │ ──▶ │ prepare_top │ ──▶ │ build_top │ ───▶ │ top_created │ ──▶ │ join │ ──▶ │ finished │ // └──────┘ └───────────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └──────┘ └──────────┘ // │ ▲ // │ │ // ▼ │ // ┌───────────────────┐ │ // │ stopwatch_running │ ───────────────────────────────────────────────────────────────────┘ // └───────────────────┘ // // make_table: // transitions: // start: // from: init // to: // - place: prepare_leg // weight: 4 // - place: prepare_top // weight: 1 // - place: stopwatch_running // weight: 1 // build_leg: // from: prepare_leg // to: leg_created // build_top: // from: prepare_top // to: top_created // join: // from: // - place: leg_created // weight: 4 // - top_created // - stopwatch_running // to: finished $definition = new Definition( [], [ new Transition('start', 'init', [new Arc('prepare_leg', 4), 'prepare_top', 'stopwatch_running']), new Transition('build_leg', 'prepare_leg', 'leg_created'), new Transition('build_top', 'prepare_top', 'top_created'), new Transition('join', [new Arc('leg_created', 4), 'top_created', 'stopwatch_running'], 'finished'), ] ); $subject = new Subject(); $workflow = new Workflow($definition); $this->assertTrue($workflow->can($subject, 'start')); $this->assertFalse($workflow->can($subject, 'build_leg')); $this->assertFalse($workflow->can($subject, 'build_top')); $this->assertFalse($workflow->can($subject, 'join')); $workflow->apply($subject, 'start'); $this->assertSame([ 'prepare_leg' => 4, 'prepare_top' => 1, 'stopwatch_running' => 1, ], $subject->getMarking()); $this->assertTrue($workflow->can($subject, 'build_leg')); $this->assertTrue($workflow->can($subject, 'build_top')); $this->assertFalse($workflow->can($subject, 'join')); $workflow->apply($subject, 'build_leg'); $this->assertSame([ 'prepare_leg' => 3, 'prepare_top' => 1, 'stopwatch_running' => 1, 'leg_created' => 1, ], $subject->getMarking()); $this->assertTrue($workflow->can($subject, 'build_leg')); $this->assertTrue($workflow->can($subject, 'build_top')); $this->assertFalse($workflow->can($subject, 'join')); $workflow->apply($subject, 'build_top'); $this->assertSame([ 'prepare_leg' => 3, 'stopwatch_running' => 1, 'leg_created' => 1, 'top_created' => 1, ], $subject->getMarking()); $this->assertTrue($workflow->can($subject, 'build_leg')); $this->assertFalse($workflow->can($subject, 'build_top')); $this->assertFalse($workflow->can($subject, 'join')); $workflow->apply($subject, 'build_leg'); $this->assertSame([ 'prepare_leg' => 2, 'stopwatch_running' => 1, 'leg_created' => 2, 'top_created' => 1, ], $subject->getMarking()); $this->assertTrue($workflow->can($subject, 'build_leg')); $this->assertFalse($workflow->can($subject, 'build_top')); $this->assertFalse($workflow->can($subject, 'join')); $workflow->apply($subject, 'build_leg'); $workflow->apply($subject, 'build_leg'); $this->assertSame([ 'stopwatch_running' => 1, 'leg_created' => 4, 'top_created' => 1, ], $subject->getMarking()); $this->assertFalse($workflow->can($subject, 'build_leg')); $this->assertFalse($workflow->can($subject, 'build_top')); $this->assertTrue($workflow->can($subject, 'join')); $workflow->apply($subject, 'join'); $this->assertSame([ 'finished' => 1, ], $subject->getMarking()); } private function assertPlaces(array $expected, Marking $marking) { $places = $marking->getPlaces(); ksort($places); $this->assertSame($expected, $places); } } class EventDispatcherMock implements EventDispatcherInterface { public array $dispatchedEvents = []; public function dispatch($event, ?string $eventName = null): object { $this->dispatchedEvents[] = $eventName; return $event; } }