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

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
[Workflow] Add transition blockers
  • Loading branch information
d-ph authored and lyrixx committed Mar 20, 2018
commit 4d10e10096f9325470564f8bc596d4f3ec06f051
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
Comment thread
lyrixx marked this conversation as resolved.
{
return 0 !== count($this->transitionBlockerList);
}

public function setBlocked(bool $blocked): void
Comment thread
lyrixx marked this conversation as resolved.
{
if (!$blocked) {
$this->transitionBlockerList = new TransitionBlockerList();

return;
}

$this->transitionBlockerList->add(TransitionBlocker::createUnknownReason($this->getTransition()->getName()));
}

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
@@ -0,0 +1,34 @@
<?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 the workflow when a transition is not enabled.
*/
class BlockedTransitionException extends LogicException
{
private $transitionBlockerList;

public function __construct(string $message, TransitionBlockerList $transitionBlockerList)
{
parent::__construct($message);

$this->transitionBlockerList = $transitionBlockerList;
}

public function getTransitionBlockerList(): TransitionBlockerList
{
return $this->transitionBlockerList;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?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.
*/
class UndefinedTransitionException extends LogicException
{
}
112 changes: 112 additions & 0 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\BlockedTransitionException;
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 @@ -411,6 +413,116 @@ public function testGetEnabledTransitionsWithSameNameTransition()
$this->assertSame('to_a', $transitions[1]->getName());
$this->assertSame('to_a', $transitions[2]->getName());
}

public function testWhyCannotReturnsReasonsProvidedInGuards()
{
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$dispatcher = new EventDispatcher();
$workflow = new Workflow($definition, new MultipleStateMarkingStore(), $dispatcher);

$guardsAddingTransitionBlockers = array(
function (GuardEvent $event) {
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 1', 'blocker_1'));
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 2', 'blocker_2'));
},
function (GuardEvent $event) {
$event->addTransitionBlocker(new TransitionBlocker('Transition blocker 3', 'blocker_3'));
},
);

foreach ($guardsAddingTransitionBlockers as $guard) {
$dispatcher->addListener('workflow.guard', $guard);
}

$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't1');

$this->assertCount(3, $transitionBlockerList);

$assertTransitionBlockerPresentByCodeFn = function (string $code) use ($transitionBlockerList) {
$this->assertNotNull(
$transitionBlockerList->findByCode($code),
sprintf('Workflow did not produce transition blocker with code "%s"', $code)
);
};

$assertTransitionBlockerPresentByCodeFn('blocker_1');
$assertTransitionBlockerPresentByCodeFn('blocker_2');
$assertTransitionBlockerPresentByCodeFn('blocker_3');
}

public function testWhyCannotReturnsTransitionNotDefinedReason()
{
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition);

$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 'undefined_transition_name');

$this->assertCount(1, $transitionBlockerList);
$this->assertEquals(
TransitionBlocker::REASON_TRANSITION_NOT_DEFINED,
$transitionBlockerList->get(0)->getCode()
);
}

public function testWhyCannotReturnsTransitionNotApplicableReason()
{
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition);

$transitionBlockerList = $workflow->buildTransitionBlockerList($subject, 't2');

$this->assertCount(1, $transitionBlockerList);
$this->assertEquals(
TransitionBlocker::REASON_TRANSITION_NOT_APPLICABLE,
$transitionBlockerList->get(0)->getCode()
);
}

public function testApplyConveysTheTransitionBlockers()
{
$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 3', 'blocker_1'));
});

try {
$workflow->apply($subject, 't1');
} catch (BlockedTransitionException $exception) {
$this->assertNotNull(
$exception->getTransitionBlockerList()->findByCode('blocker_1'),
'Workflow failed to convey it could not transition subject because of expected blocker'
);

return;
}

$this->fail('Workflow failed to prevent a transition from happening');
}

/**
* @expectedException \Symfony\Component\Workflow\Exception\UndefinedTransitionException
* @expectedExceptionMessage Transition "undefined_transition" is not defined in workflow "unnamed".
*/
public function testApplyWithUndefinedTransition()
{
$definition = $this->createSimpleWorkflowDefinition();
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow($definition);

$workflow->apply($subject, 'undefined_transition');
}
}

class EventDispatcherMock implements \Symfony\Component\EventDispatcher\EventDispatcherInterface
Expand Down
112 changes: 112 additions & 0 deletions src/Symfony/Component/Workflow/TransitionBlocker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?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.
*/
class TransitionBlocker
{
const REASON_TRANSITION_NOT_DEFINED = '80f2a8e9-ee53-408a-9dd8-cce09e031db8';
const REASON_TRANSITION_NOT_APPLICABLE = '19beefc8-6b1e-4716-9d07-a39bd6d16e34';
const REASON_TRANSITION_UNKNOWN = 'e8b5bbb9-5913-4b98-bfa6-65dbd228a82a';

private $message;
private $code;

/**
* @var array This is useful if you would like to pass around the condition values, that
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would move this doc to the constructor phpdoc instead as this var is private.

* 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.
*/
private $parameters;

public function __construct(string $message, string $code, array $parameters = array())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably document what code is and the different possibilities.

{
$this->message = $message;
$this->code = $code;
$this->parameters = $parameters;
}

/**
* Create a blocker, that says the transition cannot be made because it is undefined
* in a workflow.
*
* @param string $transitionName
* @param string $workflowName
*
* @return static
*/
public static function createNotDefined(string $transitionName, string $workflowName): self
{
$message = sprintf('Transition "%s" is not defined in workflow "%s".', $transitionName, $workflowName);
$parameters = array(
'transitionName' => $transitionName,
'workflowName' => $workflowName,
);

return new static($message, self::REASON_TRANSITION_NOT_DEFINED, $parameters);
}

/**
* Create a blocker, that says the transition cannot be made because the subject
* is in wrong place (i.e. status).
*
* @param string $transitionName
*
* @return static
*/
public static function createNotApplicable(string $transitionName): self
{
$message = sprintf('Transition "%s" cannot be made, because the subject is not in the required place.', $transitionName);
$parameters = array(
'transitionName' => $transitionName,
);

return new static($message, self::REASON_TRANSITION_NOT_APPLICABLE, $parameters);
}

/**
* Create a blocker, that says the transition cannot be made because of unknown
* reason.
*
* This blocker code is chiefly for preserving backwards compatibility.
*
* @param string $transitionName
*
* @return static
*/
public static function createUnknownReason(string $transitionName): self
{
$message = sprintf('Transition "%s" cannot be made, because of unknown reason.', $transitionName);
$parameters = array(
'transitionName' => $transitionName,
);

return new static($message, self::REASON_TRANSITION_UNKNOWN, $parameters);
}

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

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

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