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-boardingbdd3f95
Make the Workflow support State Machines
This commit is contained in:
commit
a6ea24e36f
@ -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];
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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')) {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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'])
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
}
|
18
src/Symfony/Component/Workflow/StateMachine.php
Normal file
18
src/Symfony/Component/Workflow/StateMachine.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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()
|
||||
|
75
src/Symfony/Component/Workflow/Tests/StateMachineTest.php
Normal file
75
src/Symfony/Component/Workflow/Tests/StateMachineTest.php
Normal 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 |
|
||||
// +----+ +----+
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -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 |
|
||||
// +----+ +----+
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user