Skip to content

Commit 4d10e10

Browse files
d-phlyrixx
authored andcommitted
[Workflow] Add transition blockers
1 parent 1fffb85 commit 4d10e10

File tree

9 files changed

+518
-55
lines changed

9 files changed

+518
-55
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 <[email protected]>
1621
* @author Grégoire Pineau <[email protected]>
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 0 !== count($this->transitionBlockerList);
40+
}
41+
42+
public function setBlocked(bool $blocked): void
43+
{
44+
if (!$blocked) {
45+
$this->transitionBlockerList = new TransitionBlockerList();
46+
47+
return;
48+
}
49+
50+
$this->transitionBlockerList->add(TransitionBlocker::createUnknownReason($this->getTransition()->getName()));
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
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 the workflow when a transition is not enabled.
18+
*/
19+
class BlockedTransitionException extends LogicException
20+
{
21+
private $transitionBlockerList;
22+
23+
public function __construct(string $message, TransitionBlockerList $transitionBlockerList)
24+
{
25+
parent::__construct($message);
26+
27+
$this->transitionBlockerList = $transitionBlockerList;
28+
}
29+
30+
public function getTransitionBlockerList(): TransitionBlockerList
31+
{
32+
return $this->transitionBlockerList;
33+
}
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
class UndefinedTransitionException extends LogicException
18+
{
19+
}

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

Lines changed: 112 additions & 0 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\BlockedTransitionException;
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
@@ -411,6 +413,116 @@ public function testGetEnabledTransitionsWithSameNameTransition()
411413
$this->assertSame('to_a', $transitions[1]->getName());
412414
$this->assertSame('to_a', $transitions[2]->getName());
413415
}
416+
417+
public function testWhyCannotReturnsReasonsProvidedInGuards()
418+
{
419+
$definition = $this->createSimpleWorkflowDefinition();
420+
$subject = new \stdClass();
421+
$subject->marking = null;
422+
$dispatcher = new EventDispatcher();
423+
$workflow = new Workflow($definition, new MultipleStateMarkingStore(), $dispatcher);
424+
425+
$guardsAddingTransitionBlockers = array(
426+
function (GuardEvent $event) {
427+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 1', 'blocker_1'));
428+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 2', 'blocker_2'));
429+
},
430+
function (GuardEvent $event) {
431+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3'));
432+
},
433+
);
434+
435+
foreach ($guardsAddingTransitionBlockers as $guard) {
436+
$dispatcher->addListener('workflow.guard', $guard);
437+
}
438+
439+
$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1');
440+
441+
$this->assertCount(3, $transitionBlockerList);
442+
443+
$assertTransitionBlockerPresentByCodeFn = function (string $code) use ($transitionBlockerList) {
444+
$this->assertNotNull(
445+
$transitionBlockerList->findByCode($code),
446+
sprintf('Workflow did not produce transition blocker with code "%s"', $code)
447+
);
448+
};
449+
450+
$assertTransitionBlockerPresentByCodeFn('blocker_1');
451+
$assertTransitionBlockerPresentByCodeFn('blocker_2');
452+
$assertTransitionBlockerPresentByCodeFn('blocker_3');
453+
}
454+
455+
public function testWhyCannotReturnsTransitionNotDefinedReason()
456+
{
457+
$definition = $this->createSimpleWorkflowDefinition();
458+
$subject = new \stdClass();
459+
$subject->marking = null;
460+
$workflow = new Workflow($definition);
461+
462+
$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 'undefined_transition_name');
463+
464+
$this->assertCount(1, $transitionBlockerList);
465+
$this->assertEquals(
466+
TransitionBlocker::REASON_TRANSITION_NOT_DEFINED,
467+
$transitionBlockerList->get(0)->getCode()
468+
);
469+
}
470+
471+
public function testWhyCannotReturnsTransitionNotApplicableReason()
472+
{
473+
$definition = $this->createSimpleWorkflowDefinition();
474+
$subject = new \stdClass();
475+
$subject->marking = null;
476+
$workflow = new Workflow($definition);
477+
478+
$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't2');
479+
480+
$this->assertCount(1, $transitionBlockerList);
481+
$this->assertEquals(
482+
TransitionBlocker::REASON_TRANSITION_NOT_APPLICABLE,
483+
$transitionBlockerList->get(0)->getCode()
484+
);
485+
}
486+
487+
public function testApplyConveysTheTransitionBlockers()
488+
{
489+
$definition = $this->createSimpleWorkflowDefinition();
490+
$subject = new \stdClass();
491+
$subject->marking = null;
492+
$dispatcher = new EventDispatcher();
493+
$workflow = new Workflow($definition, new MultipleStateMarkingStore(), $dispatcher);
494+
495+
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
496+
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_1'));
497+
});
498+
499+
try {
500+
$workflow->apply($subject, 't1');
501+
} catch (BlockedTransitionException $exception) {
502+
$this->assertNotNull(
503+
$exception->getTransitionBlockerList()->findByCode('blocker_1'),
504+
'Workflow failed to convey it could not transition subject because of expected blocker'
505+
);
506+
507+
return;
508+
}
509+
510+
$this->fail('Workflow failed to prevent a transition from happening');
511+
}
512+
513+
/**
514+
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
515+
* @expectedExceptionMessage Transition "undefined_transition" is not defined in workflow "unnamed".
516+
*/
517+
public function testApplyWithUndefinedTransition()
518+
{
519+
$definition = $this->createSimpleWorkflowDefinition();
520+
$subject = new \stdClass();
521+
$subject->marking = null;
522+
$workflow = new Workflow($definition);
523+
524+
$workflow->apply($subject, 'undefined_transition');
525+
}
414526
}
415527

416528
class EventDispatcherMock implements \Symfony\Component\EventDispatcher\EventDispatcherInterface
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
class TransitionBlocker
18+
{
19+
const REASON_TRANSITION_NOT_DEFINED = '80f2a8e9-ee53-408a-9dd8-cce09e031db8';
20+
const REASON_TRANSITION_NOT_APPLICABLE = '19beefc8-6b1e-4716-9d07-a39bd6d16e34';
21+
const REASON_TRANSITION_UNKNOWN = 'e8b5bbb9-5913-4b98-bfa6-65dbd228a82a';
22+
23+
private $message;
24+
private $code;
25+
26+
/**
27+
* @var array This is useful if you would like to pass around the condition values, that
28+
* blocked the transition. E.g. for a condition "distance must be larger than
29+
* 5 miles", you might want to pass around the value of 5.
30+
*/
31+
private $parameters;
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 undefined
42+
* in a workflow.
43+
*
44+
* @param string $transitionName
45+
* @param string $workflowName
46+
*
47+
* @return static
48+
*/
49+
public static function createNotDefined(string $transitionName, string $workflowName): self
50+
{
51+
$message = sprintf('Transition "%s" is not defined in workflow "%s".', $transitionName, $workflowName);
52+
$parameters = array(
53+
'transitionName' => $transitionName,
54+
'workflowName' => $workflowName,
55+
);
56+
57+
return new static($message, self::REASON_TRANSITION_NOT_DEFINED, $parameters);
58+
}
59+
60+
/**
61+
* Create a blocker, that says the transition cannot be made because the subject
62+
* is in wrong place (i.e. status).
63+
*
64+
* @param string $transitionName
65+
*
66+
* @return static
67+
*/
68+
public static function createNotApplicable(string $transitionName): self
69+
{
70+
$message = sprintf('Transition "%s" cannot be made, because the subject is not in the required place.', $transitionName);
71+
$parameters = array(
72+
'transitionName' => $transitionName,
73+
);
74+
75+
return new static($message, self::REASON_TRANSITION_NOT_APPLICABLE, $parameters);
76+
}
77+
78+
/**
79+
* Create a blocker, that says the transition cannot be made because of unknown
80+
* reason.
81+
*
82+
* This blocker code is chiefly for preserving backwards compatibility.
83+
*
84+
* @param string $transitionName
85+
*
86+
* @return static
87+
*/
88+
public static function createUnknownReason(string $transitionName): self
89+
{
90+
$message = sprintf('Transition "%s" cannot be made, because of unknown reason.', $transitionName);
91+
$parameters = array(
92+
'transitionName' => $transitionName,
93+
);
94+
95+
return new static($message, self::REASON_TRANSITION_UNKNOWN, $parameters);
96+
}
97+
98+
public function getMessage(): string
99+
{
100+
return $this->message;
101+
}
102+
103+
public function getCode(): string
104+
{
105+
return $this->code;
106+
}
107+
108+
public function getParameters(): array
109+
{
110+
return $this->parameters;
111+
}
112+
}

0 commit comments

Comments
 (0)