Skip to content

Navigation Menu

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 00530e0

Browse filesBrowse files
d-phlyrixx
d-ph
authored andcommitted
[Workflow] Add transition blockers
1 parent 4d6c481 commit 00530e0
Copy full SHA for 00530e0

9 files changed

+518
-55
lines changed

‎src/Symfony/Component/Workflow/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Workflow/CHANGELOG.md
+3-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Workflow/Event/GuardEvent.php
+36-5Lines 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 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
}
+34Lines 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 <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 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+
}
+19Lines 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 <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+
class UndefinedTransitionException extends LogicException
18+
{
19+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Workflow/Tests/WorkflowTest.php
+112Lines 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
+112Lines 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 <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+
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.