Skip to content

Commit a6ea24e

Browse files
committed
feature symfony#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 symfony#19605, Closes symfony#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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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.

0 commit comments

Comments
 (0)