feature #19629 [Workflow] Make the Workflow support State Machines (Nyholm, lyrixx)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Workflow] Make the Workflow support State Machines

| Q | A |
| --- | --- |
| Branch? | "master" |
| Bug fix? | no |
| New feature? | yes |
| BC breaks? | yes, getEnabledTransistions does not return an assoc array. |
| Deprecations? | no |
| Tests pass? | yes |
| Fixed tickets | Fixes #19605, Closes #19607 |
| License | MIT |
| Doc PR | https://github.com/symfony/symfony-docs/pull/6871 |

While researching for the docs of the component I've found that:
- A Workflow is a subclass of a Petri net
- A state machine is subclass of a Workflow
- A state machine must not be in many places simultaneously.

This PR adds a new interface to the marking store that allow us to validate the transition to true if ANY _input_ (froms) place matches the _tokens_ (marking). The default behavior is that ALL input places must match the tokens.

Commits
-------

9e49198 [Workflow] Made the code more robbust and ease on-boarding
bdd3f95 Make the Workflow support State Machines
This commit is contained in:
Fabien Potencier 2016-11-07 11:23:53 -08:00
commit a6ea24e36f
22 changed files with 627 additions and 111 deletions

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
use Symfony\Component\Workflow\Validator\SinglePlaceWorkflowValidator;
use Symfony\Component\Workflow\Validator\StateMachineValidator;
use Symfony\Component\Workflow\Validator\WorkflowValidator;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidateWorkflowsPass implements CompilerPassInterface
{
/**
* @var DefinitionValidatorInterface[]
*/
private $validators = array();
public function process(ContainerBuilder $container)
{
$taggedServices = $container->findTaggedServiceIds('workflow.definition');
foreach ($taggedServices as $id => $tags) {
$definition = $container->get($id);
foreach ($tags as $tag) {
if (!array_key_exists('name', $tag)) {
throw new RuntimeException(sprintf('The "name" for the tag "workflow.definition" of service "%s" must be set.', $id));
}
if (!array_key_exists('type', $tag)) {
throw new RuntimeException(sprintf('The "type" for the tag "workflow.definition" of service "%s" must be set.', $id));
}
if (!array_key_exists('marking_store', $tag)) {
throw new RuntimeException(sprintf('The "marking_store" for the tag "workflow.definition" of service "%s" must be set.', $id));
}
$this->getValidator($tag)->validate($definition, $tag['name']);
}
}
}
/**
* @param array $tag
*
* @return DefinitionValidatorInterface
*/
private function getValidator($tag)
{
if ($tag['type'] === 'state_machine') {
$name = 'state_machine';
$class = StateMachineValidator::class;
} elseif ($tag['marking_store'] === 'scalar') {
$name = 'single_place';
$class = SinglePlaceWorkflowValidator::class;
} else {
$name = 'workflow';
$class = WorkflowValidator::class;
}
if (empty($this->validators[$name])) {
$this->validators[$name] = new $class();
}
return $this->validators[$name];
}
}

View File

@ -236,6 +236,10 @@ class Configuration implements ConfigurationInterface
->useAttributeAsKey('name')
->prototype('array')
->children()
->enumNode('type')
->values(array('workflow', 'state_machine'))
->defaultValue('workflow')
->end()
->arrayNode('marking_store')
->isRequired()
->children()

View File

@ -404,10 +404,20 @@ class FrameworkExtension extends Extension
$registryDefinition = $container->getDefinition('workflow.registry');
foreach ($workflows as $name => $workflow) {
$type = $workflow['type'];
$definitionDefinition = new Definition(Workflow\Definition::class);
$definitionDefinition->addMethodCall('addPlaces', array($workflow['places']));
foreach ($workflow['transitions'] as $transitionName => $transition) {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
if ($type === 'workflow') {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
} elseif ($type === 'state_machine') {
foreach ($transition['from'] as $from) {
foreach ($transition['to'] as $to) {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $from, $to))));
}
}
}
}
if (isset($workflow['marking_store']['type'])) {
@ -415,17 +425,27 @@ class FrameworkExtension extends Extension
foreach ($workflow['marking_store']['arguments'] as $argument) {
$markingStoreDefinition->addArgument($argument);
}
} else {
} elseif (isset($workflow['marking_store']['service'])) {
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
}
$workflowDefinition = new DefinitionDecorator('workflow.abstract');
$definitionDefinition->addTag('workflow.definition', array(
'name' => $name,
'type' => $type,
'marking_store' => isset($workflow['marking_store']['type']) ? $workflow['marking_store']['type'] : null,
));
$definitionDefinition->setPublic(false);
$workflowDefinition = new DefinitionDecorator(sprintf('%s.abstract', $type));
$workflowDefinition->replaceArgument(0, $definitionDefinition);
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
if (isset($markingStoreDefinition)) {
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
}
$workflowDefinition->replaceArgument(3, $name);
$workflowId = 'workflow.'.$name;
$workflowId = sprintf('%s.%s', $type, $name);
$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
$container->setDefinition($workflowId, $workflowDefinition);
foreach ($workflow['supports'] as $supportedClass) {

View File

@ -35,6 +35,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumpe
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ValidateWorkflowsPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@ -93,6 +94,7 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new PropertyInfoPass());
$container->addCompilerPass(new ControllerArgumentValueResolverPass());
$container->addCompilerPass(new CachePoolPass());
$container->addCompilerPass(new ValidateWorkflowsPass());
$container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING);
if ($container->getParameter('kernel.debug')) {

View File

@ -7,7 +7,13 @@
<services>
<service id="workflow.abstract" class="Symfony\Component\Workflow\Workflow" abstract="true">
<argument /> <!-- workflow definition -->
<argument /> <!-- marking store -->
<argument type="constant">null</argument> <!-- marking store -->
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
<argument /> <!-- name -->
</service>
<service id="state_machine.abstract" class="Symfony\Component\Workflow\StateMachine" abstract="true">
<argument /> <!-- workflow definition -->
<argument type="constant">null</argument> <!-- marking store -->
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
<argument /> <!-- name -->
</service>

View File

@ -46,6 +46,9 @@ class Definition
return $this->places;
}
/**
* @return Transition[]
*/
public function getTransitions()
{
return $this->transitions;
@ -103,6 +106,6 @@ class Definition
}
}
$this->transitions[$name] = $transition;
$this->transitions[] = $transition;
}
}

View File

@ -83,9 +83,10 @@ class GraphvizDumper implements DumperInterface
{
$transitions = array();
foreach ($definition->getTransitions() as $name => $transition) {
$transitions[$name] = array(
foreach ($definition->getTransitions() as $transition) {
$transitions[] = array(
'attributes' => array('shape' => 'box', 'regular' => true),
'name' => $transition->getName(),
);
}
@ -111,10 +112,10 @@ class GraphvizDumper implements DumperInterface
{
$code = '';
foreach ($transitions as $id => $place) {
foreach ($transitions as $place) {
$code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n",
$this->dotize($id),
$id,
$this->dotize($place['name']),
$place['name'],
$this->addAttributes($place['attributes'])
);
}

View File

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 the DefinitionValidatorInterface when the definition is invalid.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class InvalidDefinitionException extends \LogicException implements ExceptionInterface
{
}

View File

@ -20,7 +20,7 @@ use Symfony\Component\Workflow\Marking;
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ScalarMarkingStore implements MarkingStoreInterface, UniqueTransitionOutputInterface
class ScalarMarkingStore implements MarkingStoreInterface
{
private $property;
private $propertyAccessor;

View File

@ -1,21 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\MarkingStore;
/**
* UniqueTransitionOutputInterface.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface UniqueTransitionOutputInterface
{
}

View File

@ -0,0 +1,18 @@
<?php
namespace Symfony\Component\Workflow;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class StateMachine extends Workflow
{
public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
{
parent::__construct($definition, $markingStore ?: new ScalarMarkingStore(), $dispatcher, $name);
}
}

View File

@ -55,7 +55,7 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$definition = new Definition($places, array($transition));
$this->assertCount(1, $definition->getTransitions());
$this->assertSame($transition, $definition->getTransitions()['name']);
$this->assertSame($transition, $definition->getTransitions()[0]);
}
/**

View File

@ -18,9 +18,9 @@ class RegistryTest extends \PHPUnit_Framework_TestCase
$this->registry = new Registry();
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow1'), Subject1::class);
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow2'), Subject2::class);
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow3'), Subject2::class);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow1'), Subject1::class);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow2'), Subject2::class);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow3'), Subject2::class);
}
protected function tearDown()
@ -55,7 +55,7 @@ class RegistryTest extends \PHPUnit_Framework_TestCase
}
/**
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedException \Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedExceptionMessage Unable to find a workflow for class "stdClass".
*/
public function testGetWithNoMatch()

View File

@ -0,0 +1,75 @@
<?php
namespace Symfony\Component\Workflow\Tests;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\StateMachine;
use Symfony\Component\Workflow\Transition;
class StateMachineTest extends \PHPUnit_Framework_TestCase
{
public function testCan()
{
$places = array('a', 'b', 'c', 'd');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t1', 'd', 'b');
$transitions[] = new Transition('t2', 'b', 'c');
$transitions[] = new Transition('t3', 'b', 'd');
$definition = new Definition($places, $transitions);
$net = new StateMachine($definition);
$subject = new \stdClass();
// If you are in place "a" you should be able to apply "t1"
$subject->marking = 'a';
$this->assertTrue($net->can($subject, 't1'));
$subject->marking = 'd';
$this->assertTrue($net->can($subject, 't1'));
$subject->marking = 'b';
$this->assertFalse($net->can($subject, 't1'));
// The graph looks like:
//
// +-------------------------------+
// v |
// +---+ +----+ +----+ +----+ +---+ +----+
// | a | --> | t1 | --> | b | --> | t3 | --> | d | --> | t1 |
// +---+ +----+ +----+ +----+ +---+ +----+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}
public function testCanWithMultipleTransition()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t2', 'a', 'c');
$definition = new Definition($places, $transitions);
$net = new StateMachine($definition);
$subject = new \stdClass();
// If you are in place "a" you should be able to apply "t1" and "t2"
$subject->marking = 'a';
$this->assertTrue($net->can($subject, 't1'));
$this->assertTrue($net->can($subject, 't2'));
// The graph looks like:
//
// +----+ +----+ +---+
// | a | --> | t1 | --> | b |
// +----+ +----+ +---+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Symfony\Component\Workflow\Tests\Validator;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Tests\WorkflowTest;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Validator\SinglePlaceWorkflowValidator;
use Symfony\Component\Workflow\Workflow;
class SinglePlaceWorkflowValidatorTest extends WorkflowTest
{
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage The marking store of workflow "foo" can not store many places.
*/
public function testSinglePlaceWorkflowValidatorAndComplexWorkflow()
{
$definition = $this->createComplexWorkflow();
(new SinglePlaceWorkflowValidator())->validate($definition, 'foo');
}
public function testSinglePlaceWorkflowValidatorAndSimpleWorkflow()
{
$places = array('a', 'b');
$transition = new Transition('t1', 'a', 'b');
$definition = new Definition($places, array($transition));
(new SinglePlaceWorkflowValidator())->validate($definition, 'foo');
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace Symfony\Component\Workflow\Tests\Validator;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Validator\StateMachineValidator;
class StateMachineValidatorTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage A transition from a place/state must have an unique name.
*/
public function testWithMultipleTransitionWithSameNameShareInput()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t1', 'a', 'c');
$definition = new Definition($places, $transitions);
(new StateMachineValidator())->validate($definition, 'foo');
// The graph looks like:
//
// +----+ +----+ +---+
// | a | --> | t1 | --> | b |
// +----+ +----+ +---+
// |
// |
// v
// +----+ +----+
// | t1 | --> | c |
// +----+ +----+
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage A transition in StateMachine can only have one output.
*/
public function testWithMultipleTos()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', 'a', array('b', 'c'));
$definition = new Definition($places, $transitions);
(new StateMachineValidator())->validate($definition, 'foo');
// The graph looks like:
//
// +---+ +----+ +---+
// | a | --> | t1 | --> | b |
// +---+ +----+ +---+
// |
// |
// v
// +----+
// | c |
// +----+
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage A transition in StateMachine can only have one input.
*/
public function testWithMultipleFroms()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', array('a', 'b'), 'c');
$definition = new Definition($places, $transitions);
(new StateMachineValidator())->validate($definition, 'foo');
// The graph looks like:
//
// +---+ +----+ +---+
// | a | --> | t1 | --> | c |
// +---+ +----+ +---+
// ^
// |
// |
// +----+
// | b |
// +----+
}
public function testValid()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t2', 'a', 'c');
$definition = new Definition($places, $transitions);
(new StateMachineValidator())->validate($definition, 'foo');
// The graph looks like:
//
// +----+ +----+ +---+
// | a | --> | t1 | --> | b |
// +----+ +----+ +---+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}
}

View File

@ -8,32 +8,11 @@ use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;
class WorkflowTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Workflow\Exception\LogicException
* @expectedExceptionMessage The marking store (Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore) of workflow "unnamed" can not store many places. But the transition "t1" has too many output (2). Only one is accepted.
*/
public function testConstructorWithUniqueTransitionOutputInterfaceAndComplexWorkflow()
{
$definition = $this->createComplexWorkflow();
new Workflow($definition, new ScalarMarkingStore());
}
public function testConstructorWithUniqueTransitionOutputInterfaceAndSimpleWorkflow()
{
$places = array('a', 'b');
$transition = new Transition('t1', 'a', 'b');
$definition = new Definition($places, array($transition));
new Workflow($definition, new ScalarMarkingStore());
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\LogicException
* @expectedExceptionMessage The value returned by the MarkingStore is not an instance of "Symfony\Component\Workflow\Marking" for workflow "unnamed".
@ -42,7 +21,7 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
{
$subject = new \stdClass();
$subject->marking = null;
$workflow = new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class));
$workflow = new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock());
$workflow->getMarking($subject);
}
@ -134,7 +113,9 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
$subject = new \stdClass();
$subject->marking = null;
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) { $event->setBlocked(true); });
$eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) {
$event->setBlocked(true);
});
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore(), $eventDispatcher, 'workflow_name');
$this->assertFalse($workflow->can($subject, 't1'));
@ -209,7 +190,9 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
$subject = new \stdClass();
$subject->marking = null;
$eventDispatcher = new EventDispatcher();
$eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) { $event->setBlocked(true); });
$eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) {
$event->setBlocked(true);
});
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore(), $eventDispatcher, 'workflow_name');
$this->assertEmpty($workflow->getEnabledTransitions($subject));
@ -217,16 +200,16 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase
$subject->marking = array('d' => true);
$transitions = $workflow->getEnabledTransitions($subject);
$this->assertCount(2, $transitions);
$this->assertSame('t3', $transitions['t3']->getName());
$this->assertSame('t4', $transitions['t4']->getName());
$this->assertSame('t3', $transitions[0]->getName());
$this->assertSame('t4', $transitions[1]->getName());
$subject->marking = array('c' => true, 'e' => true);
$transitions = $workflow->getEnabledTransitions($subject);
$this->assertCount(1, $transitions);
$this->assertSame('t5', $transitions['t5']->getName());
$this->assertSame('t5', $transitions[0]->getName());
}
private function createComplexWorkflow()
protected function createComplexWorkflow()
{
$definition = new Definition();

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Validator;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface DefinitionValidatorInterface
{
/**
* @param Definition $definition
* @param string $name
*
* @return bool
*
* @throws InvalidDefinitionException on invalid definition
*/
public function validate(Definition $definition, $name);
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Validator;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
/**
* If the marking can contain only one place, we should control the definition.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class SinglePlaceWorkflowValidator extends WorkflowValidator
{
public function validate(Definition $definition, $name)
{
foreach ($definition->getTransitions() as $transition) {
if (1 < count($transition->getTos())) {
throw new InvalidDefinitionException(
sprintf(
'The marking store of workflow "%s" can not store many places. But the transition "%s" has too many output (%d). Only one is accepted.',
$name,
$transition->getName(),
count($transition->getTos())
)
);
}
}
return parent::validate($definition, $name);
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Validator;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class StateMachineValidator implements DefinitionValidatorInterface
{
public function validate(Definition $definition, $name)
{
$transitionFromNames = array();
foreach ($definition->getTransitions() as $transition) {
// Make sure that each transition has exactly one TO
if (1 !== count($transition->getTos())) {
throw new InvalidDefinitionException(
sprintf(
'A transition in StateMachine can only have one output. But the transition "%s" in StateMachine "%s" has %d outputs.',
$transition->getName(),
$name,
count($transition->getTos())
)
);
}
// Make sure that each transition has exactly one FROM
$froms = $transition->getFroms();
if (1 !== count($froms)) {
throw new InvalidDefinitionException(
sprintf(
'A transition in StateMachine can only have one input. But the transition "%s" in StateMachine "%s" has %d inputs.',
$transition->getName(),
$name,
count($transition->getTos())
)
);
}
// Enforcing uniqueness of the names of transitions starting at each node
$from = reset($froms);
if (isset($transitionFromNames[$from][$transition->getName()])) {
throw new InvalidDefinitionException(
sprintf(
'A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" where found on StateMachine "%s". ',
$transition->getName(),
$from,
$name
)
);
}
$transitionFromNames[$from][$transition->getName()] = true;
}
return true;
}
}

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Validator;
use Symfony\Component\Workflow\Definition;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class WorkflowValidator implements DefinitionValidatorInterface
{
public function validate(Definition $definition, $name)
{
}
}

View File

@ -16,11 +16,12 @@ use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\UniqueTransitionOutputInterface;
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class Workflow
{
@ -29,21 +30,12 @@ class Workflow
private $dispatcher;
private $name;
public function __construct(Definition $definition, MarkingStoreInterface $markingStore, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
{
$this->definition = $definition;
$this->markingStore = $markingStore;
$this->markingStore = $markingStore ?: new PropertyAccessorMarkingStore();
$this->dispatcher = $dispatcher;
$this->name = $name;
// If the marking can contain only one place, we should control the definition
if ($markingStore instanceof UniqueTransitionOutputInterface) {
foreach ($definition->getTransitions() as $transition) {
if (1 < count($transition->getTos())) {
throw new LogicException(sprintf('The marking store (%s) of workflow "%s" can not store many places. But the transition "%s" has too many output (%d). Only one is accepted.', get_class($markingStore), $this->name, $transition->getName(), count($transition->getTos())));
}
}
}
}
/**
@ -102,16 +94,10 @@ class Workflow
*/
public function can($subject, $transitionName)
{
$transitions = $this->definition->getTransitions();
if (!isset($transitions[$transitionName])) {
throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name));
}
$transition = $transitions[$transitionName];
$transitions = $this->getTransitions($transitionName);
$marking = $this->getMarking($subject);
return $this->doCan($subject, $marking, $transition);
return null !== $this->getTransitionForSubject($subject, $marking, $transitions);
}
/**
@ -127,15 +113,13 @@ class Workflow
*/
public function apply($subject, $transitionName)
{
if (!$this->can($subject, $transitionName)) {
$transitions = $this->getTransitions($transitionName);
$marking = $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 "can" method already checks the Marking state
$marking = $this->markingStore->getMarking($subject);
$transition = $this->definition->getTransitions()[$transitionName];
$this->leave($subject, $transition, $marking);
$this->transition($subject, $transition, $marking);
@ -162,8 +146,8 @@ class Workflow
$marking = $this->getMarking($subject);
foreach ($this->definition->getTransitions() as $transition) {
if ($this->doCan($subject, $marking, $transition)) {
$enabled[$transition->getName()] = $transition;
if (null !== $this->getTransitionForSubject($subject, $marking, array($transition))) {
$enabled[] = $transition;
}
}
@ -175,21 +159,13 @@ class Workflow
return $this->name;
}
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
* @param Transition $transition
*
* @return bool|void boolean true if this transition is guarded, ie you cannot use it
*/
private function guardTransition($subject, Marking $marking, Transition $transition)
{
if (null === $this->dispatcher) {
@ -263,9 +239,55 @@ class Workflow
$event = new Event($subject, $marking, $initialTransition);
foreach ($this->definition->getTransitions() as $transition) {
if ($this->doCan($subject, $marking, $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;
}
}
}
}