[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:
Grégoire Pineau 2017-01-13 15:53:55 +01:00
parent 39d975b360
commit edd5431e6e
3 changed files with 180 additions and 77 deletions

View File

@ -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');

View File

@ -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)
{
}

View File

@ -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);
}
}
}