Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d36097b

Browse filesBrowse files
committed
feature #11882 [Workflow] Introducing the workflow component (fabpot, lyrixx)
This PR was merged into the 3.2-dev branch. Discussion ---------- [Workflow] Introducing the workflow component | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | not yet | Fixed tickets | n/a | License | MIT | Doc PR | n/a TODO: * [x] Add tests * [x] Add PHP doc * [x] Add Symfony fullstack integration (Config, DIC, command to dump the state-machine into graphiz format) So why another component? This component take another approach that what you can find on [Packagist](https://packagist.org/search/?q=state%20machine). Here, the workflow component is not tied to a specific object like with [Finite](https://github.com/yohang/Finite). It means that the component workflow is stateless and can be a symfony service. Some code: ```php #!/usr/bin/env php <?php require __DIR__.'/vendor/autoload.php'; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Workflow\Definition; use Symfony\Component\Workflow\Dumper\GraphvizDumper; use Symfony\Component\Workflow\Marking; use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore; use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore; use Symfony\Component\Workflow\Transition; use Symfony\Component\Workflow\Workflow; class Foo { public $marking; public function __construct($init = 'a') { $this->marking = $init; $this->marking = [$init => true]; } } $fooDefinition = new Definition(Foo::class); $fooDefinition->addPlaces([ 'a', 'b', 'c', 'd', 'e', 'f', 'g', ]); // name from to $fooDefinition->addTransition(new Transition('t1', 'a', ['b', 'c'])); $fooDefinition->addTransition(new Transition('t2', ['b', 'c'], 'd')); $fooDefinition->addTransition(new Transition('t3', 'd', 'e')); $fooDefinition->addTransition(new Transition('t4', 'd', 'f')); $fooDefinition->addTransition(new Transition('t5', 'e', 'g')); $fooDefinition->addTransition(new Transition('t6', 'f', 'g')); $graph = (new GraphvizDumper())->dump($fooDefinition); $ed = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch(), new \Monolog\Logger('app')); // $workflow = new Workflow($fooDefinition, new ScalarMarkingStore(), $ed); $workflow = new Workflow($fooDefinition, new PropertyAccessorMarkingStore(), $ed); $foo = new Foo(isset($argv[1]) ? $argv[1] : 'a'); $graph = (new GraphvizDumper())->dump($fooDefinition, $workflow->getMarking($foo)); dump([ 'AvailableTransitions' => $workflow->getAvailableTransitions($foo), 'CurrentMarking' => clone $workflow->getMarking($foo), 'can validate t1' => $workflow->can($foo, 't1'), 'can validate t3' => $workflow->can($foo, 't3'), 'can validate t6' => $workflow->can($foo, 't6'), 'apply t1' => clone $workflow->apply($foo, 't1'), 'can validate t2' => $workflow->can($foo, 't2'), 'apply t2' => clone $workflow->apply($foo, 't2'), 'can validate t1 bis' => $workflow->can($foo, 't1'), 'can validate t3 bis' => $workflow->can($foo, 't3'), 'can validate t6 bis' => $workflow->can($foo, 't6'), ]); ``` The workflown: ![workflow](https://cloud.githubusercontent.com/assets/408368/14183999/4a43483c-f773-11e5-9c8b-7f157e0cb75f.png) The output: ``` array:10 [ "AvailableTransitions" => array:1 [ 0 => Symfony\Component\Workflow\Transition {#4 -name: "t1" -froms: array:1 [ 0 => "a" ] -tos: array:2 [ 0 => "b" 1 => "c" ] } ] "CurrentMarking" => Symfony\Component\Workflow\Marking {#19 -places: array:1 [ "a" => true ] } "can validate t1" => true "can validate t3" => false "can validate t6" => false "apply t1" => Symfony\Component\Workflow\Marking {#22 -places: array:2 [ "b" => true "c" => true ] } "apply t2" => Symfony\Component\Workflow\Marking {#47 -places: array:1 [ "d" => true ] } "can validate t1 bis" => false "can validate t3 bis" => true "can validate t6 bis" => false ] ``` Commits ------- 078e27f [Workflow] Added initial set of files 17d59a7 added the first more-or-less working version of the Workflow component
2 parents 9af416d + 078e27f commit d36097b
Copy full SHA for d36097b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner

44 files changed

+2555
-0
lines changed

‎composer.json

Copy file name to clipboardExpand all lines: composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"symfony/validator": "self.version",
7373
"symfony/var-dumper": "self.version",
7474
"symfony/web-profiler-bundle": "self.version",
75+
"symfony/workflow": "self.version",
7576
"symfony/yaml": "self.version"
7677
},
7778
"require-dev": {
+52Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Bridge\Twig\Extension;
13+
14+
use Symfony\Component\Workflow\Registry;
15+
16+
/**
17+
* WorkflowExtension.
18+
*
19+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
20+
*/
21+
class WorkflowExtension extends \Twig_Extension
22+
{
23+
private $workflowRegistry;
24+
25+
public function __construct(Registry $workflowRegistry)
26+
{
27+
$this->workflowRegistry = $workflowRegistry;
28+
}
29+
30+
public function getFunctions()
31+
{
32+
return array(
33+
new \Twig_SimpleFunction('workflow_can', array($this, 'canTransition')),
34+
new \Twig_SimpleFunction('workflow_transitions', array($this, 'getEnabledTransitions')),
35+
);
36+
}
37+
38+
public function canTransition($object, $transition, $name = null)
39+
{
40+
return $this->workflowRegistry->get($object, $name)->can($object, $transition);
41+
}
42+
43+
public function getEnabledTransitions($object, $name = null)
44+
{
45+
return $this->workflowRegistry->get($object, $name)->getEnabledTransitions($object);
46+
}
47+
48+
public function getName()
49+
{
50+
return 'workflow';
51+
}
52+
}
+78Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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\Command;
13+
14+
use Symfony\Component\Console\Input\InputArgument;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
18+
use Symfony\Component\Workflow\Marking;
19+
20+
/**
21+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
22+
*/
23+
class WorkflowDumpCommand extends ContainerAwareCommand
24+
{
25+
public function isEnabled()
26+
{
27+
return $this->getContainer()->has('workflow.registry');
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
protected function configure()
34+
{
35+
$this
36+
->setName('workflow:dump')
37+
->setDefinition(array(
38+
new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'),
39+
new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'),
40+
))
41+
->setDescription('Dump a workflow')
42+
->setHelp(<<<'EOF'
43+
The <info>%command.name%</info> command dumps the graphical representation of a
44+
workflow in DOT format
45+
46+
%command.full_name% <workflow name> | dot -Tpng > workflow.png
47+
48+
EOF
49+
)
50+
;
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
protected function execute(InputInterface $input, OutputInterface $output)
57+
{
58+
$workflow = $this->getContainer()->get('workflow.'.$input->getArgument('name'));
59+
$definition = $this->getProperty($workflow, 'definition');
60+
61+
$dumper = new GraphvizDumper();
62+
63+
$marking = new Marking();
64+
foreach ($input->getArgument('marking') as $place) {
65+
$marking->mark($place);
66+
}
67+
68+
$output->writeln($dumper->dump($definition, $marking));
69+
}
70+
71+
private function getProperty($object, $property)
72+
{
73+
$reflectionProperty = new \ReflectionProperty(get_class($object), $property);
74+
$reflectionProperty->setAccessible(true);
75+
76+
return $reflectionProperty->getValue($object);
77+
}
78+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+94Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public function getConfigTreeBuilder()
103103
$this->addSsiSection($rootNode);
104104
$this->addFragmentsSection($rootNode);
105105
$this->addProfilerSection($rootNode);
106+
$this->addWorkflowSection($rootNode);
106107
$this->addRouterSection($rootNode);
107108
$this->addSessionSection($rootNode);
108109
$this->addRequestSection($rootNode);
@@ -226,6 +227,99 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode)
226227
;
227228
}
228229

230+
private function addWorkflowSection(ArrayNodeDefinition $rootNode)
231+
{
232+
$rootNode
233+
->children()
234+
->arrayNode('workflows')
235+
->useAttributeAsKey('name')
236+
->prototype('array')
237+
->children()
238+
->arrayNode('marking_store')
239+
->isRequired()
240+
->children()
241+
->enumNode('type')
242+
->values(array('property_accessor', 'scalar'))
243+
->end()
244+
->arrayNode('arguments')
245+
->beforeNormalization()
246+
->ifString()
247+
->then(function ($v) { return array($v); })
248+
->end()
249+
->prototype('scalar')
250+
->end()
251+
->end()
252+
->scalarNode('service')
253+
->cannotBeEmpty()
254+
->end()
255+
->end()
256+
->validate()
257+
->always(function ($v) {
258+
if (isset($v['type']) && isset($v['service'])) {
259+
throw new \InvalidArgumentException('"type" and "service" could not be used together.');
260+
}
261+
262+
return $v;
263+
})
264+
->end()
265+
->end()
266+
->arrayNode('supports')
267+
->isRequired()
268+
->beforeNormalization()
269+
->ifString()
270+
->then(function ($v) { return array($v); })
271+
->end()
272+
->prototype('scalar')
273+
->cannotBeEmpty()
274+
->validate()
275+
->ifTrue(function ($v) { return !class_exists($v); })
276+
->thenInvalid('The supported class %s does not exist.')
277+
->end()
278+
->end()
279+
->end()
280+
->arrayNode('places')
281+
->isRequired()
282+
->requiresAtLeastOneElement()
283+
->prototype('scalar')
284+
->cannotBeEmpty()
285+
->end()
286+
->end()
287+
->arrayNode('transitions')
288+
->useAttributeAsKey('name')
289+
->isRequired()
290+
->requiresAtLeastOneElement()
291+
->prototype('array')
292+
->children()
293+
->arrayNode('from')
294+
->beforeNormalization()
295+
->ifString()
296+
->then(function ($v) { return array($v); })
297+
->end()
298+
->requiresAtLeastOneElement()
299+
->prototype('scalar')
300+
->cannotBeEmpty()
301+
->end()
302+
->end()
303+
->arrayNode('to')
304+
->beforeNormalization()
305+
->ifString()
306+
->then(function ($v) { return array($v); })
307+
->end()
308+
->requiresAtLeastOneElement()
309+
->prototype('scalar')
310+
->cannotBeEmpty()
311+
->end()
312+
->end()
313+
->end()
314+
->end()
315+
->end()
316+
->end()
317+
->end()
318+
->end()
319+
->end()
320+
;
321+
}
322+
229323
private function addRouterSection(ArrayNodeDefinition $rootNode)
230324
{
231325
$rootNode

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+51Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
3232
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
3333
use Symfony\Component\Validator\Validation;
34+
use Symfony\Component\Workflow;
3435

3536
/**
3637
* FrameworkExtension.
3738
*
3839
* @author Fabien Potencier <fabien@symfony.com>
3940
* @author Jeremy Mikola <jmikola@gmail.com>
4041
* @author Kévin Dunglas <dunglas@gmail.com>
42+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
4143
*/
4244
class FrameworkExtension extends Extension
4345
{
@@ -129,6 +131,7 @@ public function load(array $configs, ContainerBuilder $container)
129131
$this->registerTranslatorConfiguration($config['translator'], $container);
130132
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
131133
$this->registerCacheConfiguration($config['cache'], $container);
134+
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
132135

133136
if ($this->isConfigEnabled($container, $config['router'])) {
134137
$this->registerRouterConfiguration($config['router'], $container, $loader);
@@ -346,6 +349,54 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $
346349
}
347350
}
348351

352+
/**
353+
* Loads the workflow configuration.
354+
*
355+
* @param array $workflows A workflow configuration array
356+
* @param ContainerBuilder $container A ContainerBuilder instance
357+
* @param XmlFileLoader $loader An XmlFileLoader instance
358+
*/
359+
private function registerWorkflowConfiguration(array $workflows, ContainerBuilder $container, XmlFileLoader $loader)
360+
{
361+
if (!$workflows) {
362+
return;
363+
}
364+
365+
$loader->load('workflow.xml');
366+
367+
$registryDefinition = $container->getDefinition('workflow.registry');
368+
369+
foreach ($workflows as $name => $workflow) {
370+
$definitionDefinition = new Definition(Workflow\Definition::class);
371+
$definitionDefinition->addMethodCall('addPlaces', array($workflow['places']));
372+
foreach ($workflow['transitions'] as $transitionName => $transition) {
373+
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
374+
}
375+
376+
if (isset($workflow['marking_store']['type'])) {
377+
$markingStoreDefinition = new DefinitionDecorator('workflow.marking_store.'.$workflow['marking_store']['type']);
378+
foreach ($workflow['marking_store']['arguments'] as $argument) {
379+
$markingStoreDefinition->addArgument($argument);
380+
}
381+
} else {
382+
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
383+
}
384+
385+
$workflowDefinition = new DefinitionDecorator('workflow.abstract');
386+
$workflowDefinition->replaceArgument(0, $definitionDefinition);
387+
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
388+
$workflowDefinition->replaceArgument(3, $name);
389+
390+
$workflowId = 'workflow.'.$name;
391+
392+
$container->setDefinition($workflowId, $workflowDefinition);
393+
394+
foreach ($workflow['supports'] as $supportedClass) {
395+
$registryDefinition->addMethodCall('add', array(new Reference($workflowId), $supportedClass));
396+
}
397+
}
398+
}
399+
349400
/**
350401
* Loads the router configuration.
351402
*

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+39Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<xsd:element name="serializer" type="serializer" minOccurs="0" maxOccurs="1" />
2727
<xsd:element name="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
2828
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
29+
<xsd:element name="workflows" type="workflows" minOccurs="0" maxOccurs="1" />
2930
</xsd:all>
3031

3132
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@@ -224,4 +225,42 @@
224225
<xsd:attribute name="provider" type="xsd:string" />
225226
<xsd:attribute name="clearer" type="xsd:string" />
226227
</xsd:complexType>
228+
229+
<xsd:complexType name="workflows">
230+
<xsd:choice minOccurs="0" maxOccurs="unbounded">
231+
<xsd:element name="workflow" type="workflow" />
232+
</xsd:choice>
233+
</xsd:complexType>
234+
235+
<xsd:complexType name="workflow">
236+
<xsd:sequence>
237+
<xsd:element name="marking-store" type="marking_store" />
238+
<xsd:element name="supports" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
239+
<xsd:element name="places" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
240+
<xsd:element name="transitions" type="transitions" />
241+
</xsd:sequence>
242+
<xsd:attribute name="name" type="xsd:string" />
243+
</xsd:complexType>
244+
245+
<xsd:complexType name="marking_store">
246+
<xsd:sequence>
247+
<xsd:element name="type" type="xsd:string" minOccurs="0" maxOccurs="1" />
248+
<xsd:element name="arguments" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
249+
<xsd:element name="service" type="xsd:string" minOccurs="0" maxOccurs="1" />
250+
</xsd:sequence>
251+
</xsd:complexType>
252+
253+
<xsd:complexType name="transitions">
254+
<xsd:sequence>
255+
<xsd:element name="transition" type="transition" />
256+
</xsd:sequence>
257+
</xsd:complexType>
258+
259+
<xsd:complexType name="transition">
260+
<xsd:sequence>
261+
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
262+
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
263+
</xsd:sequence>
264+
<xsd:attribute name="name" type="xsd:string" />
265+
</xsd:complexType>
227266
</xsd:schema>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.