[Workflow] Fixed support of multiple transition with the same name.
The previous behavior was underterministic because it took the first transition during the `can` and the `apply` method. But the "first" does not mean anything. Now the workflow apply all possible transitions with the same name.
This commit is contained in:
parent
39d975b360
commit
edd5431e6e
@ -49,6 +49,34 @@ trait WorkflowBuilderTrait
|
||||
// +---+ +----+ +---+ +----+ +---+
|
||||
}
|
||||
|
||||
private function createWorkflowWithSameNameTransition()
|
||||
{
|
||||
$places = range('a', 'c');
|
||||
|
||||
$transitions = array();
|
||||
$transitions[] = new Transition('a_to_bc', 'a', array('b', 'c'));
|
||||
$transitions[] = new Transition('b_to_c', 'b', 'c');
|
||||
$transitions[] = new Transition('to_a', 'b', 'a');
|
||||
$transitions[] = new Transition('to_a', 'c', 'a');
|
||||
|
||||
return new Definition($places, $transitions);
|
||||
|
||||
// The graph looks like:
|
||||
// +------------------------------------------------------------+
|
||||
// | |
|
||||
// | |
|
||||
// | +----------------------------------------+ |
|
||||
// v | v |
|
||||
// +---+ +---------+ +---+ +--------+ +---+ +------+
|
||||
// | a | --> | a_to_bc | --> | b | --> | b_to_c | --> | c | --> | to_a | -+
|
||||
// +---+ +---------+ +---+ +--------+ +---+ +------+ |
|
||||
// ^ | ^ |
|
||||
// | +----------------------------------+ |
|
||||
// | |
|
||||
// | |
|
||||
// +--------------------------------------------------------------------+
|
||||
}
|
||||
|
||||
private function createComplexStateMachineDefinition()
|
||||
{
|
||||
$places = array('a', 'b', 'c', 'd');
|
||||
|
@ -48,7 +48,7 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
public function testGetMarkingWithImpossiblePlace()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = array('nope' => true);
|
||||
$subject->marking = array('nope' => 1);
|
||||
$workflow = new Workflow(new Definition(array(), array()), new MultipleStateMarkingStore());
|
||||
|
||||
$workflow->getMarking($subject);
|
||||
@ -83,10 +83,6 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertTrue($marking->has('c'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Transition "foobar" does not exist for workflow "unnamed".
|
||||
*/
|
||||
public function testCanWithUnexistingTransition()
|
||||
{
|
||||
$definition = $this->createComplexWorkflowDefinition();
|
||||
@ -94,7 +90,7 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
|
||||
|
||||
$workflow->can($subject, 'foobar');
|
||||
$this->assertFalse($workflow->can($subject, 'foobar'));
|
||||
}
|
||||
|
||||
public function testCan()
|
||||
@ -136,6 +132,23 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
$workflow->apply($subject, 't2');
|
||||
}
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
public function testApply()
|
||||
{
|
||||
$definition = $this->createComplexWorkflowDefinition();
|
||||
@ -151,6 +164,59 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertTrue($marking->has('c'));
|
||||
}
|
||||
|
||||
public function testApplyWithSameNameTransition()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$definition = $this->createWorkflowWithSameNameTransition();
|
||||
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
|
||||
|
||||
$marking = $workflow->apply($subject, 'a_to_bc');
|
||||
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertTrue($marking->has('b'));
|
||||
$this->assertTrue($marking->has('c'));
|
||||
|
||||
$marking = $workflow->apply($subject, 'to_a');
|
||||
|
||||
$this->assertTrue($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertFalse($marking->has('c'));
|
||||
|
||||
$marking = $workflow->apply($subject, 'a_to_bc');
|
||||
$marking = $workflow->apply($subject, 'b_to_c');
|
||||
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertTrue($marking->has('c'));
|
||||
|
||||
$marking = $workflow->apply($subject, 'to_a');
|
||||
|
||||
$this->assertTrue($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertFalse($marking->has('c'));
|
||||
}
|
||||
|
||||
public function testApplyWithSameNameTransition2()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = array('a' => 1, 'b' => 1);
|
||||
|
||||
$places = range('a', 'd');
|
||||
$transitions = array();
|
||||
$transitions[] = new Transition('t', 'a', 'c');
|
||||
$transitions[] = new Transition('t', 'b', 'd');
|
||||
$definition = new Definition($places, $transitions);
|
||||
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
|
||||
|
||||
$marking = $workflow->apply($subject, 't');
|
||||
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertTrue($marking->has('c'));
|
||||
$this->assertTrue($marking->has('d'));
|
||||
}
|
||||
|
||||
public function testApplyWithEventDispatcher()
|
||||
{
|
||||
$definition = $this->createComplexWorkflowDefinition();
|
||||
@ -198,17 +264,36 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$this->assertEmpty($workflow->getEnabledTransitions($subject));
|
||||
|
||||
$subject->marking = array('d' => true);
|
||||
$subject->marking = array('d' => 1);
|
||||
$transitions = $workflow->getEnabledTransitions($subject);
|
||||
$this->assertCount(2, $transitions);
|
||||
$this->assertSame('t3', $transitions[0]->getName());
|
||||
$this->assertSame('t4', $transitions[1]->getName());
|
||||
|
||||
$subject->marking = array('c' => true, 'e' => true);
|
||||
$subject->marking = array('c' => 1, 'e' => 1);
|
||||
$transitions = $workflow->getEnabledTransitions($subject);
|
||||
$this->assertCount(1, $transitions);
|
||||
$this->assertSame('t5', $transitions[0]->getName());
|
||||
}
|
||||
|
||||
public function testGetEnabledTransitionsWithSameNameTransition()
|
||||
{
|
||||
$definition = $this->createWorkflowWithSameNameTransition();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new MultipleStateMarkingStore());
|
||||
|
||||
$transitions = $workflow->getEnabledTransitions($subject);
|
||||
$this->assertCount(1, $transitions);
|
||||
$this->assertSame('a_to_bc', $transitions[0]->getName());
|
||||
|
||||
$subject->marking = array('b' => 1, 'c' => 1);
|
||||
$transitions = $workflow->getEnabledTransitions($subject);
|
||||
$this->assertCount(3, $transitions);
|
||||
$this->assertSame('b_to_c', $transitions[0]->getName());
|
||||
$this->assertSame('to_a', $transitions[1]->getName());
|
||||
$this->assertSame('to_a', $transitions[2]->getName());
|
||||
}
|
||||
}
|
||||
|
||||
class EventDispatcherMock implements \Symfony\Component\EventDispatcher\EventDispatcherInterface
|
||||
@ -223,21 +308,27 @@ class EventDispatcherMock implements \Symfony\Component\EventDispatcher\EventDis
|
||||
public function addListener($eventName, $listener, $priority = 0)
|
||||
{
|
||||
}
|
||||
|
||||
public function addSubscriber(\Symfony\Component\EventDispatcher\EventSubscriberInterface $subscriber)
|
||||
{
|
||||
}
|
||||
|
||||
public function removeListener($eventName, $listener)
|
||||
{
|
||||
}
|
||||
|
||||
public function removeSubscriber(\Symfony\Component\EventDispatcher\EventSubscriberInterface $subscriber)
|
||||
{
|
||||
}
|
||||
|
||||
public function getListeners($eventName = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function getListenerPriority($eventName, $listener)
|
||||
{
|
||||
}
|
||||
|
||||
public function hasListeners($eventName = null)
|
||||
{
|
||||
}
|
||||
|
@ -89,15 +89,18 @@ class Workflow
|
||||
* @param string $transitionName A transition
|
||||
*
|
||||
* @return bool true if the transition is enabled
|
||||
*
|
||||
* @throws LogicException If the transition does not exist
|
||||
*/
|
||||
public function can($subject, $transitionName)
|
||||
{
|
||||
$transitions = $this->getTransitions($transitionName);
|
||||
$marking = $this->getMarking($subject);
|
||||
$transitions = $this->getEnabledTransitions($subject, $this->getMarking($subject));
|
||||
|
||||
return null !== $this->getTransitionForSubject($subject, $marking, $transitions);
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transitionName === $transition->getName()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,22 +116,36 @@ class Workflow
|
||||
*/
|
||||
public function apply($subject, $transitionName)
|
||||
{
|
||||
$transitions = $this->getTransitions($transitionName);
|
||||
$marking = $this->getMarking($subject);
|
||||
$transitions = $this->getEnabledTransitions($subject, $this->getMarking($subject));
|
||||
|
||||
if (null === $transition = $this->getTransitionForSubject($subject, $marking, $transitions)) {
|
||||
throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
|
||||
// We can shortcut the getMarking method in order to boost performance,
|
||||
// since the "getEnabledTransitions" method already checks the Marking
|
||||
// state
|
||||
$marking = $this->markingStore->getMarking($subject);
|
||||
|
||||
$applied = false;
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
if ($transitionName !== $transition->getName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$applied = true;
|
||||
|
||||
$this->leave($subject, $transition, $marking);
|
||||
|
||||
$this->transition($subject, $transition, $marking);
|
||||
|
||||
$this->enter($subject, $transition, $marking);
|
||||
|
||||
$this->markingStore->setMarking($subject, $marking);
|
||||
|
||||
$this->announce($subject, $transition, $marking);
|
||||
}
|
||||
|
||||
$this->leave($subject, $transition, $marking);
|
||||
|
||||
$this->transition($subject, $transition, $marking);
|
||||
|
||||
$this->enter($subject, $transition, $marking);
|
||||
|
||||
$this->markingStore->setMarking($subject, $marking);
|
||||
|
||||
$this->announce($subject, $transition, $marking);
|
||||
if (!$applied) {
|
||||
throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
|
||||
}
|
||||
|
||||
return $marking;
|
||||
}
|
||||
@ -146,7 +163,7 @@ class Workflow
|
||||
$marking = $this->getMarking($subject);
|
||||
|
||||
foreach ($this->definition->getTransitions() as $transition) {
|
||||
if (null !== $this->getTransitionForSubject($subject, $marking, array($transition))) {
|
||||
if ($this->doCan($subject, $marking, $transition)) {
|
||||
$enabled[] = $transition;
|
||||
}
|
||||
}
|
||||
@ -167,6 +184,21 @@ class Workflow
|
||||
return $this->definition;
|
||||
}
|
||||
|
||||
private function doCan($subject, Marking $marking, Transition $transition)
|
||||
{
|
||||
foreach ($transition->getFroms() as $place) {
|
||||
if (!$marking->has($place)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (true === $this->guardTransition($subject, $marking, $transition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $subject
|
||||
* @param Marking $marking
|
||||
@ -246,56 +278,8 @@ class Workflow
|
||||
|
||||
$event = new Event($subject, $marking, $initialTransition);
|
||||
|
||||
foreach ($this->definition->getTransitions() as $transition) {
|
||||
if (null !== $this->getTransitionForSubject($subject, $marking, array($transition))) {
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()), $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $transitionName
|
||||
*
|
||||
* @return Transition[]
|
||||
*/
|
||||
private function getTransitions($transitionName)
|
||||
{
|
||||
$transitions = $this->definition->getTransitions();
|
||||
|
||||
$transitions = array_filter($transitions, function (Transition $transition) use ($transitionName) {
|
||||
return $transitionName === $transition->getName();
|
||||
});
|
||||
|
||||
if (!$transitions) {
|
||||
throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name));
|
||||
}
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first Transition in $transitions that is valid for the
|
||||
* $subject and $marking. null is returned when you cannot do any Transition
|
||||
* in $transitions on the $subject.
|
||||
*
|
||||
* @param object $subject
|
||||
* @param Marking $marking
|
||||
* @param Transition[] $transitions
|
||||
*
|
||||
* @return Transition|null
|
||||
*/
|
||||
private function getTransitionForSubject($subject, Marking $marking, array $transitions)
|
||||
{
|
||||
foreach ($transitions as $transition) {
|
||||
foreach ($transition->getFroms() as $place) {
|
||||
if (!$marking->has($place)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (true !== $this->guardTransition($subject, $marking, $transition)) {
|
||||
return $transition;
|
||||
}
|
||||
foreach ($this->getEnabledTransitions($subject) as $transition) {
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()), $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user