Permalink
Browse files

feature #19629 [Workflow] Make the Workflow support State Machines (N…

…yholm, 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 | symfony/symfony-docs#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
  • Loading branch information...
fabpot committed Nov 7, 2016
2 parents 53b55fc + 9e49198 commit a6ea24e36fb4a4e2726cfc441fd17b1dc877f3c4
Showing with 627 additions and 111 deletions.
  1. +77 −0 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php
  2. +4 −0 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
  3. +25 −5 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
  4. +2 −0 src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
  5. +7 −1 src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml
  6. +4 −1 src/Symfony/Component/Workflow/Definition.php
  7. +6 −5 src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
  8. +21 −0 src/Symfony/Component/Workflow/Exception/InvalidDefinitionException.php
  9. +1 −1 src/Symfony/Component/Workflow/MarkingStore/ScalarMarkingStore.php
  10. +0 −21 src/Symfony/Component/Workflow/MarkingStore/UniqueTransitionOutputInterface.php
  11. +18 −0 src/Symfony/Component/Workflow/StateMachine.php
  12. +1 −1 src/Symfony/Component/Workflow/Tests/DefinitionTest.php
  13. +4 −4 src/Symfony/Component/Workflow/Tests/RegistryTest.php
  14. +75 −0 src/Symfony/Component/Workflow/Tests/StateMachineTest.php
  15. +33 −0 src/Symfony/Component/Workflow/Tests/Validator/SinglePlaceWorkflowValidatorTest.php
  16. +108 −0 src/Symfony/Component/Workflow/Tests/Validator/StateMachineValidatorTest.php
  17. +11 −28 src/Symfony/Component/Workflow/Tests/WorkflowTest.php
  18. +31 −0 src/Symfony/Component/Workflow/Validator/DefinitionValidatorInterface.php
  19. +41 −0 src/Symfony/Component/Workflow/Validator/SinglePlaceWorkflowValidator.php
  20. +68 −0 src/Symfony/Component/Workflow/Validator/StateMachineValidator.php
  21. +24 −0 src/Symfony/Component/Workflow/Validator/WorkflowValidator.php
  22. +66 −44 src/Symfony/Component/Workflow/Workflow.php
@@ -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 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->useAttributeAsKey('name')
->prototype('array')
->children()
->enumNode('type')
->values(array('workflow', 'state_machine'))
->defaultValue('workflow')
->end()
->arrayNode('marking_store')
->isRequired()
->children()
@@ -404,28 +404,48 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde
$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'])) {
$markingStoreDefinition = new DefinitionDecorator('workflow.marking_store.'.$workflow['marking_store']['type']);
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\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 @@ public function build(ContainerBuilder $container)
$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 @@ public function getPlaces()
return $this->places;
}
/**
* @return Transition[]
*/
public function getTransitions()
{
return $this->transitions;
@@ -103,6 +106,6 @@ public function addTransition(Transition $transition)
}
}
$this->transitions[$name] = $transition;
$this->transitions[] = $transition;
}
}
@@ -83,9 +83,10 @@ private function findTransitions(Definition $definition)
{
$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 @@ private function addTransitions(array $transitions)
{
$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 @@
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ScalarMarkingStore implements MarkingStoreInterface, UniqueTransitionOutputInterface
class ScalarMarkingStore implements MarkingStoreInterface
{
private $property;
private $propertyAccessor;

This file was deleted.

Oops, something went wrong.
@@ -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 @@ public function testAddTransition()
$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 @@ protected function setUp()
$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 @@ public function testGetWithMultipleMatch()
}
/**
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedException \Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedExceptionMessage Unable to find a workflow for class "stdClass".
*/
public function testGetWithNoMatch()
@@ -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 |
// +----+ +----+
}
}
Oops, something went wrong.

0 comments on commit a6ea24e

Please sign in to comment.