8000 feature #19629 [Workflow] Make the Workflow support State Machines (N… · symfony/symfony@a6ea24e · GitHub
[go: up one dir, main page]

Skip to content

Commit a6ea24e

Browse files
committed
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 | 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
2 parents 53b55fc + 9e49198 commit a6ea24e

22 files changed

+627
-111
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
17+
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
18+
use Symfony\Component\Workflow\Validator\SinglePlaceWorkflowValidator;
19+
use Symfony\Component\Workflow\Validator\StateMachineValidator;
20+
use Symfony\Component\Workflow\Validator\WorkflowValidator;
21+
22+
/**
23+
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
24+
*/
25+
class ValidateWorkflowsPass implements CompilerPassInterface
26+
{
27+
/**
28+
* @var DefinitionValidatorInterface[]
29+
*/
30+
private $validators = array();
31+
32+
public function process(ContainerBuilder $container)
33+
{
34+
$taggedServices = $container->findTaggedServiceIds('workflow.definition');
35+
foreach ($taggedServices as $id => $tags) {
36+
$definition = $container->get($id);
37+
foreach ($tags as $tag) {
38+
if (!array_key_exists('name', $tag)) {
39+
throw new RuntimeException(sprintf('The "name" for the tag "workflow.definition" of service "%s" must be set.', $id));
40+
}
41+
if (!array_key_exists('type', $tag)) {
42+
throw new RuntimeException(sprintf('The "type" for the tag "workflow.definition" of service "%s" must be set.', $id));
43+
}
44+
if (!array_key_exists('marking_store', $tag)) {
45+
throw new RuntimeException(sprintf('The "marking_store" for the tag "workflow.definition" of service "%s" must be set.', $id));
46+
}
47+
48+
$this->getValidator($tag)->validate($definition, $tag['name']);
49+
}
50+
}
51+
}
52+
53+
/**
54+
* @param array $tag
55+
*
56+
* @return DefinitionValidatorInterface
57+
*/
58+
private function getValidator($tag)
59+
{
60+
if ($tag['type'] === 'state_machine') {
61+
$name = 'state_machine';
62+
$class = StateMachineValidator::class;
63+
} elseif ($tag['marking_store'] === 'scalar') {
64+
$name = 'single_place';
65+
$class = SinglePlaceWorkflowValidator::class;
66+
} else {
67+
$name = 'workflow';
68+
$class = WorkflowValidator::class;
69+
}
70+
71+
if (empty($this->validators[$name])) {
72+
$this->validators[$name] = new $class();
73+
}
74+
75+
return $this->validators[$name];
76+
}
77+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
236236
->useAttributeAsKey('name')
237237
->prototype('array')
238238
->children()
239+
->enumNode('type')
240+
->values(array('workflow', 'state_machine'))
241+
->defaultValue('workflow')
242+
->end()
239243
->arrayNode('marking_store')
240244
->isRequired()
241245
->children()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,28 +404,48 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde
404404
$registryDefinition = $container->getDefinition('workflow.registry');
405405

406406
foreach ($workflows as $name => $workflow) {
407+
$type = $workflow['type'];
408+
407409
$definitionDefinition = new Definition(Workflow\Definition::class);
408410
$definitionDefinition->addMethodCall('addPlaces', array($workflow['places']));
409411
foreach ($workflow['transitions'] as $transitionName => $transition) {
410-
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
412+
if ($type === 'workflow') {
413+
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
414+
} elseif ($type === 'state_machine') {
415+
foreach ($transition['from'] as $from) {
416+
foreach ($transition['to'] as $to) {
417+
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $from, $to))));
418+
}
419+
}
420+
}
411421
}
412422

413423
if (isset($workflow['marking_store']['type'])) {
414424
$markingStoreDefinition = new DefinitionDecorator('workflow.marking_store.'.$workflow['marking_store']['type']);
415425
foreach ($workflow['marking_store']['arguments'] as $argument) {
416426
$markingStoreDefinition->addArgument($argument);
417427
}
418-
} else {
428+
} elseif (isset($workflow['marking_store']['service'])) {
419429
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
420430
}
421431

422-
$workflowDefinition = new DefinitionDecorator('workflow.abstract');
432+
$definitionDefinition->addTag('workflow.definition', array(
433+
'name' => $name,
434+
'type' => $type,
435+
'marking_store' => isset($workflow['marking_store']['type']) ? $workflow['marking_store']['type'] : null,
436+
));
437+
$definitionDefinition->setPublic(false);
438+
439+
$workflowDefinition = new DefinitionDecorator(sprintf('%s.abstract', $type));
423440
$workflowDefinition->replaceArgument(0, $definitionDefinition);
424-
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
441+
if (isset($markingStoreDefinition)) {
442+
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
443+
}
425444
$workflowDefinition->replaceArgument(3, $name);
426445

427-
$workflowId = 'workflow.'.$name;
446+
$workflowId = sprintf('%s.%s', $type, $name);
428447

448+
$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
429449
$container->setDefinition($workflowId, $workflowDefinition);
430450

431451
foreach ($workflow['supports'] as $supportedClass) {

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass;
3636
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
3737
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass;
38+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ValidateWorkflowsPass;
3839
use Symfony\Component\Debug\ErrorHandler;
3940
use Symfony\Component\DependencyInjection\ContainerBuilder;
4041
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -93,6 +94,7 @@ public function build(ContainerBuilder $container)
9394
$container->addCompilerPass(new PropertyInfoPass());
9495
$container->addCompilerPass(new ControllerArgumentValueResolverPass());
9596
$container->addCompilerPass(new CachePoolPass());
97+
$container->addCompilerPass(new ValidateWorkflowsPass());
9698
$container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING);
9799

98100
if ($container->getParameter('kernel.debug')) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
<services>
88
<service id="workflow.abstract" class="Symfony\Component\Workflow\Workflow" abstract="true">
99
<argument /> <!-- workflow definition -->
10-
<argument /> <!-- marking store -->
10+
<argument type="constant">null</argument> <!-- marking store -->
11+
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
12+
<argument /> <!-- name -->
13+
</service>
14+
<service id="state_machine.abstract" class="Symfony\Component\Workflow\StateMachine" abstract="true">
15+
<argument /> <!-- workflow definition -->
16+
<argument type="constant">null</argument> <!-- marking store -->
1117
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
1218
<argument /> <!-- name -->
1319
</service>

src/Symfony/Component/Workflow/Definition.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public function getPlaces()
4646
return $this->places;
4747
}
4848

49+
/**
50+
* @return Transition[]
51+
*/
4952
public function getTransitions()
5053
{
5154
return $this->transitions;
@@ -103,6 +106,6 @@ public function addTransition(Transition $transition)
103106
}
104107
}
105108

106-
$this->transitions[$name] = $transition;
109+
$this->transitions[] = $transition;
107110
}
108111
}

src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ private function findTransitions(Definition $definition)
8383
{
8484
$transitions = array();
8585

86-
foreach ($definition->getTransitions() as $name => $transition) {
87-
$transitions[$name] = array(
86+
foreach ($definition->getTransitions() as $transition) {
87+
$transitions[] = array(
8888
'attributes' => array('shape' => 'box', 'regular' => true),
89+
'name' => $transition->getName(),
8990
);
9091
}
9192

@@ -111,10 +112,10 @@ private function addTransitions(array $transitions)
111112
{
112113
$code = '';
113114

114-
foreach ($transitions as $id => $place) {
115+
foreach ($transitions as $place) {
115116
$code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n",
116-
$this->dotize($id),
117-
$id,
117+
$this->dotize($place['name']),
118+
$place['name'],
118119
$this->addAttributes($place['attributes'])
119120
);
120121
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Exception;
13+
14+
/**
15+
* Thrown by the DefinitionValidatorInterface when the definition is invalid.
16+
*
17+
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
18+
*/
19+
class InvalidDefinitionException extends \LogicException implements ExceptionInterface
20+
{
21+
}

src/Symfony/Component/Workflow/MarkingStore/ScalarMarkingStore.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*
2121
* @author Grégoire Pineau <lyrixx@lyrixx.info>
2222
*/
23-
class ScalarMarkingStore implements MarkingStoreInterface, UniqueTransitionOutputInterface
23+
class ScalarMarkingStore implements MarkingStoreInterface
2424
{
2525
private $property;
2626
private $propertyAccessor;

src/Symfony/Component/Workflow/MarkingStore/UniqueTransitionOutputInterface.php

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\Workflow;
4+
5+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
6+
use Symfony\< 48DA span class=pl-v>Component\Workflow\MarkingStore\MarkingStoreInterface;
7+
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
8+
9+
/**
10+
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
11+
*/
12+
class StateMachine extends Workflow
13+
{
14+
public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
15+
{
16+
parent::__construct($definition, $markingStore ?: new ScalarMarkingStore(), $dispatcher, $name);
17+
}
18+
}

src/Symfony/Component/Workflow/Tests/DefinitionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function testAddTransition()
5555
$definition = new Definition($places, array($transition));
5656

5757
$this->assertCount(1, $definition->getTransitions());
58-
$this->assertSame($transition, $definition->getTransitions()['name']);
58+
$this->assertSame($transition, $definition->getTransitions()[0]);
5959
}
6060

6161
/**

src/Symfony/Component/Workflow/Tests/RegistryTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ protected function setUp()
1818

1919
$this->registry = new Registry();
2020

21-
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow1'), Subject1::class);
22-
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow2'), Subject2::class);
23-
$this->registry->add(new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class), $this->getMock(EventDispatcherInterface::class), 'workflow3'), Subject2::class);
21+
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow1'), Subject1::class);
22+
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow2'), Subject2::class);
23+
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow3'), Subject2::class);
2424
}
2525

2626
protected function tearDown()
@@ -55,7 +55,7 @@ public function testGetWithMultipleMatch()
5555
}
5656

5757
/**
58-
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
58+
* @expectedException \Symfony\Component\Workflow\Exception\InvalidArgumentException
5959
* @expectedExceptionMessage Unable to find a workflow for class "stdClass".
6060
*/
6161
public function testGetWithNoMatch()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Symfony\Component\Workflow\Tests;
4+
5+
use Symfony\Component\Workflow\Definition;
6+
use Symfony\Component\Workflow\Marking;
7+
use Symfony\Component\Workflow\StateMachine;
8+
use Symfony\Component\Workflow\Transition;
9+
10+
class StateMachineTest extends \PHPUnit_Framework_TestCase
11+
{
12+
public function testCan()
13+
{
14+
$places = array('a', 'b', 'c', 'd');
15+
$transitions[] = new Transition('t1', 'a', 'b');
16+
$transitions[] = new Transition('t1', 'd', 'b');
17+
$transitions[] = new Transition('t2', 'b', 'c');
18+
$transitions[] = new Transition('t3', 'b', 'd');
19+
$definition = new Definition($places, $transitions);
20+
21+
$net = new StateMachine($definition);
22+
$subject = new \stdClass();
23+
24+
// If you are in place "a" you should be able to apply "t1"
25+
$subject->marking = 'a';
26+
$this->assertTrue($net->can($subject, 't1'));
27+
$subject->marking = 'd';
28+
$this->assertTrue($net->can($subject, 't1'));
29+
30+
$subject->marking = 'b';
31+
$this->assertFalse($net->can($subject, 't1'));
32+
33+
// The graph looks like:
34+
//
35+
// +-------------------------------+
36+
// v |
37+
// +---+ +----+ +----+ +----+ +---+ +----+
38+
// | a | --> | t1 | --> | b | --> | t3 | --> | d | --> | t1 |
39+
// +---+ +----+ +----+ +----+ +---+ +----+
40+
// |
41+
// |
42+
// v
43+
// +----+ +----+
44+
// | t2 | --> | c |
45+
// +----+ +----+
46+
}
47+
48+
public function testCanWithMultipleTransition()
49+
{
50+
$places = array('a', 'b', 'c');
51+
$transitions[] = new Transition('t1', 'a', 'b');
52+
$transitions[] = new Transition('t2', 'a', 'c');
53+
$definition = new Definition($places, $transitions);
54+
55+
$net = new StateMachine($definition);
56+
$subject = new \stdClass();
57+
58+
// If you are in place "a" you should be able to apply "t1" and "t2"
59+
$subject->marking = 'a';
60+
$this->assertTrue($net->can($subject, 't1'));
61+
$this->assertTrue($net->can($subject, 't2'));
62+
63+
// The graph looks like:
64+
//
65+
// +----+ +----+ +---+
66+
// | a | --> | t1 | --> | b |
67+
// +----+ +----+ +---+
68+
// |
69+
// |
70+
// v
71+
// +----+ +----+
72+
// | t2 | --> | c |
73+
// +----+ +----+
74+
}
75+
}

0 commit comments

Comments
 (0)
0