Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Workflow] Add transition blockers #26076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Symfony/Component/Workflow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ CHANGELOG
4.1.0
-----

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

4.0.0
-----
Expand Down
41 changes: 36 additions & 5 deletions src/Symfony/Component/Workflow/Event/GuardEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,52 @@

namespace Symfony\Component\Workflow\Event;

use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\TransitionBlocker;
use Symfony\Component\Workflow\TransitionBlockerList;

/**
* @author Fabien Potencier <[email protected]>
* @author Grégoire Pineau <[email protected]>
*/
class GuardEvent extends Event
{
private $blocked = false;
private $transitionBlockerList;

/**
* {@inheritdoc}
*/
public function __construct($subject, Marking $marking, Transition $transition, $workflowName = 'unnamed')
{
parent::__construct($subject, $marking, $transition, $workflowName);

$this->transitionBlockerList = new TransitionBlockerList();
}

public function isBlocked(): bool
{
return !$this->transitionBlockerList->isEmpty();
}

public function setBlocked(bool $blocked): void
{
if (!$blocked) {
$this->transitionBlockerList->reset();

return;
}

$this->transitionBlockerList->add(TransitionBlocker::createUnknown());
}

public function isBlocked()
public function getTransitionBlockerList(): TransitionBlockerList
{
return $this->blocked;
return $this->transitionBlockerList;
}

public function setBlocked($blocked)
public function addTransitionBlocker(TransitionBlocker $transitionBlocker): void
{
$this->blocked = (bool) $blocked;
$this->transitionBlockerList->add($transitionBlocker);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Exception\InvalidTokenConfigurationException;
use Symfony\Component\Workflow\TransitionBlocker;

/**
* @author Grégoire Pineau <[email protected]>
Expand Down Expand Up @@ -49,8 +50,11 @@ public function onTransition(GuardEvent $event, $eventName)
return;
}

if (!$this->expressionLanguage->evaluate($this->configuration[$eventName], $this->getVariables($event))) {
$event->setBlocked(true);
$expression = $this->configuration[$eventName];

if (!$this->expressionLanguage->evaluate($expression, $this->getVariables($event))) {
$blocker = TransitionBlocker::createBlockedByExpressionGuardListener($expression);
$event->addTransitionBlocker($blocker);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\Exception;

use Symfony\Component\Workflow\TransitionBlockerList;

/**
* Thrown by Workflow when a not enabled transition is applied on a subject.
*
* @author Grégoire Pineau <[email protected]>
*/
class NotEnabledTransitionException extends LogicException
{
private $transitionBlockerList;

public function __construct(string $transitionName, string $workflowName, TransitionBlockerList $transitionBlockerList)
{
parent::__construct(sprintf('Transition "%s" is not enabled for workflow "%s".', $transitionName, $workflowName));

$this->transitionBlockerList = $transitionBlockerList;
}

public function getTransitionBlockerList(): TransitionBlockerList
{
return $this->transitionBlockerList;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\Exception;

/**
* Thrown by Workflow when an undefined transition is applied on a subject.
*
* @author Grégoire Pineau <[email protected]>
*/
class UndefinedTransitionException extends LogicException
{
public function __construct(string $transitionName, string $workflowName)
{
parent::__construct(sprintf('Transition "%s" is not defined for workflow "%s".', $transitionName, $workflowName));
}
}
107 changes: 94 additions & 13 deletions src/Symfony/Component/Workflow/Tests/WorkflowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\MultipleStateMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\TransitionBlocker;
use Symfony\Component\Workflow\Workflow;

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

public function testCanWithSameNameTransition()
{
$definition = $this->createWorkflowWithSameNameTransition();
$workflow = new Workflow($definition, new MultipleStateMarkingStore());

$subject = new \stdClass();
$subject->marking = null;
$this->assertTrue($workflow->can($subject, 'a_to_bc'));
$this->assertFalse($workflow->can($subject, 'b_to_c'));
$this->assertFalse($workflow->can($subject, 'to_a'));

$subject->marking = array('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'));
}

/**
* @expectedException \Symfony\Component\Workflow\Exception\LogicException
* @expectedExceptionMessage Unable to apply transition "t2" for workflow "unnamed".
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
* @expectedExceptionMessage Transition "404 Not Found" is not defined for workflow "unnamed".
*/
public function testApplyWithImpossibleTransition()
public function testBuildTransitionBlockerListReturnsUndefinedTransition()
{
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition);

$workflow->buildTransitionBlockerList($subject, '404 Not Found');
}

public function testBuildTransitionBlockerListReturnsReasonsProvidedByMarking()
{
$definition = $this->createComplexWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition, new MultipleStateMarkingStore());

$workflow->apply($subject, 't2');
$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 testCanWithSameNameTransition()
public function testBuildTransitionBlockerListReturnsReasonsProvidedInGuards()
{
$definition = $this->createWorkflowWithSameNameTransition();
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$dispatcher = new EventDispatcher();
$workflow = new Workflow($definition, new MultipleStateMarkingStore(), $dispatcher);

$dispatcher->addListener('workflow.guard', 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', function (GuardEvent $event) {
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3'));
});
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
$event->setBlocked(true);
});

$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1');
$this->assertCount(4, $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('Unknown reason.', $blockers[3]->getMessage());
$this->assertSame('e8b5bbb9-5913-4b98-bfa6-65dbd228a82a', $blockers[3]->getCode());
}

/**
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
* @expectedExceptionMessage Transition "404 Not Found" is not defined for workflow "unnamed".
*/
public function testApplyWithNotExisingTransition()
{
$definition = $this->createComplexWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition, new MultipleStateMarkingStore());

$workflow->apply($subject, '404 Not Found');
}

public function testApplyWithNotEnabledTransition()
{
$definition = $this->createComplexWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$this->assertTrue($workflow->can($subject, 'a_to_bc'));
$this->assertFalse($workflow->can($subject, 'b_to_c'));
$this->assertFalse($workflow->can($subject, 'to_a'));
$workflow = new Workflow($definition, new MultipleStateMarkingStore());

$subject->marking = array('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'));
try {
$workflow->apply($subject, 't2');

$this->fail('Should throw an exception');
} catch (NotEnabledTransitionException $e) {
$this->assertSame('Transition "t2" is not enabled for 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());
}
}

public function testApply()
Expand Down
91 changes: 91 additions & 0 deletions src/Symfony/Component/Workflow/TransitionBlocker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow;

/**
* A reason why a transition cannot be performed for a subject.
*/
final class TransitionBlocker
{
const BLOCKED_BY_MARKING = '19beefc8-6b1e-4716-9d07-a39bd6d16e34';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was in the initial PR.

It's used to identify a blocker by a "machine". It's the same approach as in the Validator component.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make them public explicitly

const BLOCKED_BY_EXPRESSION_GUARD_LISTENER = '326a1e9c-0c12-11e8-ba89-0ed5f89f718b';
const UNKNOWN = 'e8b5bbb9-5913-4b98-bfa6-65dbd228a82a';

private $message;
private $code;
private $parameters;

/**
* @param string $code Code is a machine-readable string, usually an UUID
* @param array $parameters This is useful if you would like to pass around the condition values, that
* blocked the transition. E.g. for a condition "distance must be larger than
* 5 miles", you might want to pass around the value of 5.
*/
public function __construct(string $message, string $code, array $parameters = array())
{
$this->message = $message;
$this->code = $code;
$this->parameters = $parameters;
}

/**
* Create a blocker that says the transition cannot be made because it is
* not enabled.
*
* It means the subject is in wrong place (i.e. status):
* * If the workflow is a state machine: the subject is not in the previous place of the transition.
* * If the workflow is a workflow: the subject is not in all previous places of the transition.
*/
public static function createBlockedByMarking(Marking $marking): self
{
return new static('The marking does not enable the transition.', self::BLOCKED_BY_MARKING, array(
'marking' => $marking,
));
}

/**
* Creates a blocker that says the transition cannot be made because it has
* been blocked by the expression guard listener.
*/
public static function createBlockedByExpressionGuardListener(string $expression): self
{
return new static('The expression blocks the transition.', self::BLOCKED_BY_EXPRESSION_GUARD_LISTENER, array(
'expression' => $expression,
));
}

/**
* Creates a blocker that says the transition cannot be made because of an
* unknown reason.
*
* This blocker code is chiefly for preserving backwards compatibility.
*/
public static function createUnknown(): self
{
return new static('Unknown reason.', self::UNKNOWN);
}

public function getMessage(): string
{
return $this->message;
}

public function getCode(): string
{
return $this->code;
}

public function getParameters(): array
{
return $this->parameters;
}
}
Loading