8000 feature #26076 [Workflow] Add transition blockers (d-ph, lyrixx) · symfony/symfony@5605d2f · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5605d2f

Browse files
committed
feature #26076 [Workflow] Add transition blockers (d-ph, lyrixx)
This PR was merged into the 4.1-dev branch. Discussion ---------- [Workflow] Add transition blockers | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #24745 #24501 | License | MIT Commits ------- 2b8faff [Workflow] Cleaned the transition blocker implementations 4d10e10 [Workflow] Add transition blockers
2 parents a5dbc68 + 2b8faff commit 5605d2f

File tree

10 files changed

+438
-65
lines changed

10 files changed

+438
-65
lines changed

src/Symfony/Component/Workflow/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ CHANGELOG
44
4.1.0
55
-----
66

7-
* Deprecate the usage of `add(Workflow $workflow, $supportStrategy)` in `Workflow/Registry`, use `addWorkflow(WorkflowInterface, $supportStrategy)` instead.
7+
* Deprecate the usage of `add(Workflow $workflow, $supportStrategy)` in `Workflow/Registry`, use `addWorkflow(WorkflowInterface, $supportStrategy)` instead.
88
* Deprecate the usage of `SupportStrategyInterface`, use `WorkflowSupportStrategyInterface` instead.
99
* The `Workflow` class now implements `WorkflowInterface`.
1010
* Deprecated the class `ClassInstanceSupportStrategy` in favor of the class `InstanceOfSupportStrategy`.
11+
* Added TransitionBlockers as a way to pass around reasons why exactly
12+
transitions can't be made.
1113

1214
4.0.0
1315
-----

src/Symfony/Component/Workflow/Event/GuardEvent.php

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,52 @@
1111

1212
namespace Symfony\Component\Workflow\Event;
1313

14+
use Symfony\Component\Workflow\Marking;
15+
use Symfony\Component\Workflow\Transition;
16+
use Symfony\Component\Workflow\TransitionBlocker;
17+
use Symfony\Component\Workflow\TransitionBlockerList;
18+
1419
/**
1520
* @author Fabien Potencier <fabien@symfony.com>
1621
* @author Grégoire Pineau <lyrixx@lyrixx.info>
1722
*/
1823
class GuardEvent extends Event
1924
{
20-
private $blocked = false;
25+
private $transitionBlockerList;
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function __construct($subject, Marking $marking, Transition $transition, $workflowName = 'unnamed')
31+
{
32+
parent::__construct($subject, $marking, $transition, $workflowName);
33+
34+
$this->transitionBlockerList = new TransitionBlockerList();
35+
}
36+
37+
public function isBlocked(): bool
38+
{
39+
return !$this->transitionBlockerList->isEmpty();
40+
}
41+
42+
public function setBlocked(bool $blocked): void
43+
{
44+
if (!$blocked) {
45+
$this->transitionBlockerList->reset();
46+
47+
return;
48+
}
49+
50+
$this->transitionBlockerList->add(TransitionBlocker::createUnknown());
51+
}
2152

22-
public function isBlocked()
53+
public function getTransitionBlockerList(): TransitionBlockerList
2354
{
24-
return $this->blocked;
55+
return $this->transitionBlockerList;
2556
}
2657

27-
public function setBlocked($blocked)
58+
public function addTransitionBlocker(TransitionBlocker $transitionBlocker): void
2859
{
29-
$this->blocked = (bool) $blocked;
60+
$this->transitionBlockerList->add($transitionBlocker);
3061
}
3162
}

src/Symfony/Component/Workflow/EventListener/GuardListener.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\Validator\Validator\ValidatorInterface;
1919
use Symfony\Component\Workflow\Event\GuardEvent;
2020
use Symfony\Component\Workflow\Exception\InvalidTokenConfigurationException;
21+
use Symfony\Component\Workflow\TransitionBlocker;
2122

2223
/**
2324
* @author Grégoire Pineau <lyrixx@lyrixx.info>
@@ -49,8 +50,11 @@ public function onTransition(GuardEvent $event, $eventName)
4950
return;
5051
}
5152

52-
if (!$this->expressionLanguage->evaluate($this->configuration[$eventName], $this->getVariables($event))) {
53-
$event->setBlocked(true);
53+
$expression = $this->configuration[$eventName];
54+
55+
if (!$this->expressionLanguage->evaluate($expression, $this->getVariables($event))) {
56+
$blocker = TransitionBlocker::createBlockedByExpressionGuardListener($expression);
57+
$event->addTransitionBlocker($blocker);
5458
}
5559
}
5660

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Exception;
13+
14+
use Symfony\Component\Workflow\TransitionBlockerList;
15+
16+
/**
17+
* Thrown by Workflow when a not enabled transition is applied on a subject.
18+
*
19+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
20+
*/
21+
class NotEnabledTransitionException extends LogicException
22+
{
23+
private $transitionBlockerList;
24+
25+
public function __construct(string $transitionName, string $workflowName, TransitionBlockerList $transitionBlockerList)
26+
{
27+
parent::__construct(sprintf('Transition "%s" is not enabled for workflow "%s".', $transitionName, $workflowName));
28+
29+
$this->transitionBlockerList = $transitionBlockerList;
30+
}
31+
32+
public function getTransitionBlockerList(): TransitionBlockerList
33+
{
34+
return $this->transitionBlockerList;
35+
}
36+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Exception;
13+
14+
/**
15+
* Thrown by Workflow when an undefined transition is applied on a subject.
16+
*
17+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
18+
*/
19+
class UndefinedTransitionException extends LogicException
20+
{
21+
public function __construct(string $transitionName, string $workflowName)
22+
{
23+
parent::__construct(sprintf('Transition "%s" is not defined for workflow "%s".', $transitionName, $workflowName));
24+
}
25+
}

src/Symfony/Component/Workflow/Tests/WorkflowTest.php

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
use Symfony\Component\Workflow\Definition;
88
use Symfony\Component\Workflow\Event\Event;
99
use Symfony\Component\Workflow\Event\GuardEvent;
10+
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
1011
use Symfony\Component\Workflow\Marking;
1112
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
1213
use Symfony\Component\Workflow\MarkingStore\MultipleStateMarkingStore;
1314
use Symfony\Component\Workflow\Transition;
15+
use Symfony\Component\Workflow\TransitionBlocker;
1416
use Symfony\Component\Workflow\Workflow;
1517

1618
class WorkflowTest extends TestCase
@@ -162,35 +164,114 @@ public function testCanDoesNotTriggerGuardEventsForNotEnabledTransitions()
162164
$this->assertSame(array('workflow_name.guard.t3'), $dispatchedEvents);
163165
}
164166

167+
public function testCanWithSameNameTransition()
168+
{
169+
$definition = $this->createWorkflowWithSameNameTransition();
170+
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
171+
172+
$subject = new \stdClass();
173+
$subject->marking = null;
174+
$this->assertTrue($workflow->can($subject, 'a_to_bc'));
175+
$this->assertFalse($workflow->can($subject, 'b_to_c'));
176+
$this->assertFalse($workflow->can($subject, 'to_a'));
177+
178+
$subject->marking = array('b' => 1);
179+
$this->assertFalse($workflow->can($subject, 'a_to_bc'));
180+
$this->assertTrue($workflow->can($subject, 'b_to_c'));
181+
$this->assertTrue($workflow->can($subject, 'to_a'));
182+
}
183+
165184
/**
166-
* @expectedException \Symfony\Component\Workflow\Exception\LogicException
167-
* @expectedExceptionMessage Unable to apply transition "t2" for workflow "unnamed".
185+
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
186+
* @expectedExceptionMessage Transition "404 Not Found" is not defined for workflow "unnamed".
168187
*/
169-
public function testApplyWithImpossibleTransition()
188+
public function testBuildTransitionBlockerListReturnsUndefinedTransition()
189+
{
190+
$definition = $this->createSimpleWorkflowDefinition();
191+
$subject = new \stdClass();
192+
$subject->marking = null;
193+
$workflow = new Workflow($definition);
194+
195+
$workflow->buildTransitionBlockerList($subject, '404 Not Found');
196+
}
197+
198+
public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking()
170199
{
171200
$definition = $this->createComplexWorkflowDefinition();
172201
$subject = new \stdClass();
173202
$subject->marking = null;
174203
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
175204

176-
$workflow->apply($subject, 't2');
205+
$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't2');
206+
$this->assertCount(1, $transitionBlockerList);
207+
$blockers = iterator_to_array($transitionBlockerList);
208+
$this->assertSame('The marking does not enable the transition.', $blockers[0]->getMessage());
209+
$this->assertSame('19beefc8-6b1e-4716-9d07-a39bd6d16e34', $blockers[0]->getCode());
177210
}
178211

179-
public function testCanWithSameNameTransition()
212+
public function testBuildTransitionBlockerListReturnsReasonsProvidedInGuards()
180213
{
181-
$definition = $this->createWorkflowWithSameNameTransition();
214+
$definition = $this->createSimpleWorkflowDefinition();
215+
$subject = new \stdClass();
216+
$subject->marking = null;
217+
$dispatcher = new EventDispatcher();
218+
$workflow = new Workflow($definition, new MultipleStateMarkingStore(), $dispatcher);
219+
220+
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
221+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 1', 'blocker_1'));
222+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 2', 'blocker_2'));
223+
});
224+
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
225+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3'));
226+
});
227+
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
228+
$event->setBlocked(true);
229+
});
230+
231+
$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1');
232+
$this->assertCount(4, $transitionBlockerList);
233+
$blockers = iterator_to_array($transitionBlockerList);
234+
$this->assertSame('Transition blocker 1', $blockers[0]->getMessage());
235+
$this->assertSame('blocker_1', $blockers[0]->getCode());
236+
$this->assertSame('Transition blocker 2', $blockers[1]->getMessage());
237+
$this->assertSame('blocker_2', $blockers[1]->getCode());
238+
$this->assertSame('Transition blocker 3', $blockers[2]->getMessage());
239+
$this->assertSame('blocker_3', $blockers[2]->getCode());
240+
$this->assertSame('Unknown reason.', $blockers[3]->getMessage());
241+
$this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[3]->getCode());
242+
}
243+
244+
/**
245+
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
246+
* @expectedExceptionMessage Transition "404 Not Found" is not defined for workflow "unnamed".
247+
*/
248+
public function testApplyWithNotExisingTransition()
249+
{
250+
$definition = $this->createComplexWorkflowDefinition();
251+
$subject = new \stdClass();
252+
$subject->marking = null;
182253
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
183254

255+
$workflow->apply($subject, '404 Not Found');
256+
}
257+
258+
public function testApplyWithNotEnabledTransition()
259+
{
260+
$definition = $this->createComplexWorkflowDefinition();
184261
$subject = new \stdClass();
185262
$subject->marking = null;
186-
$this->assertTrue($workflow->can($subject, 'a_to_bc'));
187-
$this->assertFalse($workflow->can($subject, 'b_to_c'));
188-
$this->assertFalse($workflow->can($subject, 'to_a'));
263+
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
189264

190-
$subject->marking = array('b' => 1);
191-
$this->assertFalse($workflow->can($subject, 'a_to_bc'));
192-
$this->assertTrue($workflow->can($subject, 'b_to_c'));
193-
$this->assertTrue($workflow->can($subject, 'to_a'));
265+
try {
266+
$workflow->apply($subject, 't2');
267+
268+
$this->fail('Should throw an exception');
269+
} catch (NotEnabledTransitionException $e) {
270+
$this->assertSame('Transition "t2" is not enabled for workflow "unnamed".', $e->getMessage());
271+
$this->assertCount(1, $e->getTransitionBlockerList());
272+
$list = iterator_to_array($e->getTransitionBlockerList());
273+
$this->assertSame('The marking does not enable the transition.', $list[0]->getMessage());
274+
}
194275
}
195276

196277
public function testApply()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow;
13+
14+
/**
15+
* A reason why a transition cannot be performed for a subject.
16+
*/
17+
final class TransitionBlocker
18+
{
19+
const BLOCKED_BY_MARKING = '19beefc8-6b1e-4716-9d07-a39bd6d16e34';
20+
const BLOCKED_BY_EXPRESSION_GUARD_LISTENER = '326a1e9c-0c12-11e8-ba89-0ed5f89f718b';
21+
const UNKNOWN = 'e8b5bbb9-5913-4b98-bfa6-65dbd228a82a';
22+
23+
private $message;
24+
private $code;
25+
private $parameters;
26+
27+
/**
28+
* @param string $code Code is a machine-readable string, usually an UUID
29+
* @param array $parameters This is useful if you would like to pass around the condition values, that
30+
* blocked the transition. E.g. for a condition "distance must be larger than
31+
* 5 miles", you might want to pass around the value of 5.
32+
*/
33+
public function __construct(string $message, string $code, array $parameters = array())
34+
{
35+
$this->message = $message;
36+
$this->code = $code;
37+
$this->parameters = $parameters;
38+
}
39+
40+
/**
41+
* Create a blocker that says the transition cannot be made because it is
42+
* not enabled.
43+
*
44+
* It means the subject is in wrong place (i.e. status):
45+
* * If the workflow is a state machine: the subject is not in the previous place of the transition.
46+
* * If the workflow is a workflow: the subject is not in all previous places of the transition.
47+
*/
48+
public static function createBlockedByMarking(Marking $marking): self
49+
{
50+
return new static('The marking does not enable the transition.', self::BLOCKED_BY_MARKING, array(
51+
'marking' => $marking,
52+
));
53+
}
54+
55+
/**
56+
* Creates a blocker that says the transition cannot be made because it has
57+
* been blocked by the expression guard listener.
58+
*/
59+
public static function createBlockedByExpressionGuardListener(string $expression): self
60+
{
61+
return new static('The expression blocks the transition.', self::BLOCKED_BY_EXPRESSION_GUARD_LISTENER, array(
62+
'expression' => $expression,
63+
));
64+
}
65+
66+
/**
67+
* Creates a blocker that says the transition cannot be made because of an
68+
* unknown reason.
69+
*
70+
* This blocker code is chiefly for preserving backwards compatibility.
71+
*/
72+
public static function createUnknown(): self
73+
{
74+
return new static('Unknown reason.', self::UNKNOWN);
75+
}
76+
77+
public function getMessage(): string
78+
{
79+
re 76D3 turn $this->message;
80+
}
81+
82+
public function getCode(): string
83+
{
84+
return $this->code;
85+
}
86+
87+
public function getParameters(): array
88+
{
89+
return $this->parameters;
90+
}
91+
}

0 commit comments

Comments
 (0)
0