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 files17d59a7
added the first more-or-less working version of the Workflow component
This commit is contained in:
commit
d36097b3ef
@ -72,6 +72,7 @@
|
||||
"symfony/validator": "self.version",
|
||||
"symfony/var-dumper": "self.version",
|
||||
"symfony/web-profiler-bundle": "self.version",
|
||||
"symfony/workflow": "self.version",
|
||||
"symfony/yaml": "self.version"
|
||||
},
|
||||
"require-dev": {
|
||||
|
52
src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php
Normal file
52
src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?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\Bridge\Twig\Extension;
|
||||
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
/**
|
||||
* WorkflowExtension.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class WorkflowExtension extends \Twig_Extension
|
||||
{
|
||||
private $workflowRegistry;
|
||||
|
||||
public function __construct(Registry $workflowRegistry)
|
||||
{
|
||||
$this->workflowRegistry = $workflowRegistry;
|
||||
}
|
||||
|
||||
public function getFunctions()
|
||||
{
|
||||
return array(
|
||||
new \Twig_SimpleFunction('workflow_can', array($this, 'canTransition')),
|
||||
new \Twig_SimpleFunction('workflow_transitions', array($this, 'getEnabledTransitions')),
|
||||
);
|
||||
}
|
||||
|
||||
public function canTransition($object, $transition, $name = null)
|
||||
{
|
||||
return $this->workflowRegistry->get($object, $name)->can($object, $transition);
|
||||
}
|
||||
|
||||
public function getEnabledTransitions($object, $name = null)
|
||||
{
|
||||
return $this->workflowRegistry->get($object, $name)->getEnabledTransitions($object);
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'workflow';
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class WorkflowDumpCommand extends ContainerAwareCommand
|
||||
{
|
||||
public function isEnabled()
|
||||
{
|
||||
return $this->getContainer()->has('workflow.registry');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('workflow:dump')
|
||||
->setDefinition(array(
|
||||
new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'),
|
||||
new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'),
|
||||
))
|
||||
->setDescription('Dump a workflow')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command dumps the graphical representation of a
|
||||
workflow in DOT format
|
||||
|
||||
%command.full_name% <workflow name> | dot -Tpng > workflow.png
|
||||
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$workflow = $this->getContainer()->get('workflow.'.$input->getArgument('name'));
|
||||
$definition = $this->getProperty($workflow, 'definition');
|
||||
|
||||
$dumper = new GraphvizDumper();
|
||||
|
||||
$marking = new Marking();
|
||||
foreach ($input->getArgument('marking') as $place) {
|
||||
$marking->mark($place);
|
||||
}
|
||||
|
||||
$output->writeln($dumper->dump($definition, $marking));
|
||||
}
|
||||
|
||||
private function getProperty($object, $property)
|
||||
{
|
||||
$reflectionProperty = new \ReflectionProperty(get_class($object), $property);
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
return $reflectionProperty->getValue($object);
|
||||
}
|
||||
}
|
@ -103,6 +103,7 @@ class Configuration implements ConfigurationInterface
|
||||
$this->addSsiSection($rootNode);
|
||||
$this->addFragmentsSection($rootNode);
|
||||
$this->addProfilerSection($rootNode);
|
||||
$this->addWorkflowSection($rootNode);
|
||||
$this->addRouterSection($rootNode);
|
||||
$this->addSessionSection($rootNode);
|
||||
$this->addRequestSection($rootNode);
|
||||
@ -226,6 +227,99 @@ class Configuration implements ConfigurationInterface
|
||||
;
|
||||
}
|
||||
|
||||
private function addWorkflowSection(ArrayNodeDefinition $rootNode)
|
||||
{
|
||||
$rootNode
|
||||
->children()
|
||||
->arrayNode('workflows')
|
||||
->useAttributeAsKey('name')
|
||||
->prototype('array')
|
||||
->children()
|
||||
->arrayNode('marking_store')
|
||||
->isRequired()
|
||||
->children()
|
||||
->enumNode('type')
|
||||
->values(array('property_accessor', 'scalar'))
|
||||
->end()
|
||||
->arrayNode('arguments')
|
||||
->beforeNormalization()
|
||||
->ifString()
|
||||
->then(function ($v) { return array($v); })
|
||||
->end()
|
||||
->prototype('scalar')
|
||||
->end()
|
||||
->end()
|
||||
->scalarNode('service')
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->end()
|
||||
->validate()
|
||||
->always(function ($v) {
|
||||
if (isset($v['type']) && isset($v['service'])) {
|
||||
throw new \InvalidArgumentException('"type" and "service" could not be used together.');
|
||||
}
|
||||
|
||||
return $v;
|
||||
})
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('supports')
|
||||
->isRequired()
|
||||
->beforeNormalization()
|
||||
->ifString()
|
||||
->then(function ($v) { return array($v); })
|
||||
->end()
|
||||
->prototype('scalar')
|
||||
->cannotBeEmpty()
|
||||
->validate()
|
||||
->ifTrue(function ($v) { return !class_exists($v); })
|
||||
->thenInvalid('The supported class %s does not exist.')
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('places')
|
||||
->isRequired()
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('scalar')
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('transitions')
|
||||
->useAttributeAsKey('name')
|
||||
->isRequired()
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('array')
|
||||
->children()
|
||||
->arrayNode('from')
|
||||
->beforeNormalization()
|
||||
->ifString()
|
||||
->then(function ($v) { return array($v); })
|
||||
->end()
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('scalar')
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->end()
|
||||
->arrayNode('to')
|
||||
->beforeNormalization()
|
||||
->ifString()
|
||||
->then(function ($v) { return array($v); })
|
||||
->end()
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('scalar')
|
||||
->cannotBeEmpty()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
private function addRouterSection(ArrayNodeDefinition $rootNode)
|
||||
{
|
||||
$rootNode
|
||||
|
@ -31,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
use Symfony\Component\Workflow;
|
||||
|
||||
/**
|
||||
* FrameworkExtension.
|
||||
@ -38,6 +39,7 @@ use Symfony\Component\Validator\Validation;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jeremy Mikola <jmikola@gmail.com>
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class FrameworkExtension extends Extension
|
||||
{
|
||||
@ -129,6 +131,7 @@ class FrameworkExtension extends Extension
|
||||
$this->registerTranslatorConfiguration($config['translator'], $container);
|
||||
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
|
||||
$this->registerCacheConfiguration($config['cache'], $container);
|
||||
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
|
||||
|
||||
if ($this->isConfigEnabled($container, $config['router'])) {
|
||||
$this->registerRouterConfiguration($config['router'], $container, $loader);
|
||||
@ -346,6 +349,54 @@ class FrameworkExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the workflow configuration.
|
||||
*
|
||||
* @param array $workflows A workflow configuration array
|
||||
* @param ContainerBuilder $container A ContainerBuilder instance
|
||||
* @param XmlFileLoader $loader An XmlFileLoader instance
|
||||
*/
|
||||
private function registerWorkflowConfiguration(array $workflows, ContainerBuilder $container, XmlFileLoader $loader)
|
||||
{
|
||||
if (!$workflows) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loader->load('workflow.xml');
|
||||
|
||||
$registryDefinition = $container->getDefinition('workflow.registry');
|
||||
|
||||
foreach ($workflows as $name => $workflow) {
|
||||
$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 (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 {
|
||||
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
|
||||
}
|
||||
|
||||
$workflowDefinition = new DefinitionDecorator('workflow.abstract');
|
||||
$workflowDefinition->replaceArgument(0, $definitionDefinition);
|
||||
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
|
||||
$workflowDefinition->replaceArgument(3, $name);
|
||||
|
||||
$workflowId = 'workflow.'.$name;
|
||||
|
||||
$container->setDefinition($workflowId, $workflowDefinition);
|
||||
|
||||
foreach ($workflow['supports'] as $supportedClass) {
|
||||
$registryDefinition->addMethodCall('add', array(new Reference($workflowId), $supportedClass));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the router configuration.
|
||||
*
|
||||
|
@ -26,6 +26,7 @@
|
||||
<xsd:element name="serializer" type="serializer" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="workflows" type="workflows" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:all>
|
||||
|
||||
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
||||
@ -224,4 +225,42 @@
|
||||
<xsd:attribute name="provider" type="xsd:string" />
|
||||
<xsd:attribute name="clearer" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="workflows">
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="workflow" type="workflow" />
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="workflow">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="marking-store" type="marking_store" />
|
||||
<xsd:element name="supports" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||
<xsd:element name="places" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||
<xsd:element name="transitions" type="transitions" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="marking_store">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="type" type="xsd:string" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="arguments" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="service" type="xsd:string" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="transitions">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="transition" type="transition" />
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="transition">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<service id="workflow.abstract" class="Symfony\Component\Workflow\Workflow" abstract="true">
|
||||
<argument /> <!-- workflow definition -->
|
||||
<argument /> <!-- marking store -->
|
||||
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
|
||||
<argument /> <!-- name -->
|
||||
</service>
|
||||
|
||||
<service id="workflow.marking_store.property_accessor" class="Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore" abstract="true" />
|
||||
<service id="workflow.marking_store.scalar" class="Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore" abstract="true" />
|
||||
|
||||
<service id="workflow.registry" class="Symfony\Component\Workflow\Registry" />
|
||||
|
||||
<service id="workflow.twig_extension" class="Symfony\Bridge\Twig\Extension\WorkflowExtension">
|
||||
<argument type="service" id="workflow.registry" />
|
||||
<tag name="twig.extension" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
@ -273,6 +273,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
|
||||
'directory' => '%kernel.cache_dir%/pools',
|
||||
'default_redis_provider' => 'redis://localhost',
|
||||
),
|
||||
'workflows' => array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest;
|
||||
|
||||
$container->loadFromExtension('framework', array(
|
||||
'workflows' => array(
|
||||
'my_workflow' => array(
|
||||
'marking_store' => array(
|
||||
'type' => 'property_accessor',
|
||||
),
|
||||
'supports' => array(
|
||||
FrameworkExtensionTest::class,
|
||||
),
|
||||
'places' => array(
|
||||
'first',
|
||||
'last',
|
||||
),
|
||||
'transitions' => array(
|
||||
'go' => array(
|
||||
'from' => array(
|
||||
'first',
|
||||
),
|
||||
'to' => array(
|
||||
'last',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||
|
||||
<framework:config>
|
||||
<framework:workflows>
|
||||
<framework:workflow name="my_workflow">
|
||||
<framework:marking-store>
|
||||
<framework:type>property_accessor</framework:type>
|
||||
<framework:arguments>a</framework:arguments>
|
||||
<framework:arguments>a</framework:arguments>
|
||||
</framework:marking-store>
|
||||
<framework:supports>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:supports>
|
||||
<framework:places>first</framework:places>
|
||||
<framework:places>last</framework:places>
|
||||
<framework:transitions>
|
||||
<framework:transition name="foobar">
|
||||
<framework:from>a</framework:from>
|
||||
<framework:to>a</framework:to>
|
||||
</framework:transition>
|
||||
</framework:transitions>
|
||||
</framework:workflow>
|
||||
</framework:workflows>
|
||||
</framework:config>
|
||||
</container>
|
@ -0,0 +1,16 @@
|
||||
framework:
|
||||
workflows:
|
||||
my_workflow:
|
||||
marking_store:
|
||||
type: property_accessor
|
||||
supports:
|
||||
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
|
||||
places:
|
||||
- first
|
||||
- last
|
||||
transitions:
|
||||
go:
|
||||
from:
|
||||
- first
|
||||
to:
|
||||
- last
|
@ -117,6 +117,13 @@ abstract class FrameworkExtensionTest extends TestCase
|
||||
$this->assertFalse($container->hasDefinition('data_collector.config'), '->registerProfilerConfiguration() does not load collectors.xml');
|
||||
}
|
||||
|
||||
public function testWorkflow()
|
||||
{
|
||||
$container = $this->createContainerFromFile('workflow');
|
||||
|
||||
$this->assertTrue($container->hasDefinition('workflow.my_workflow'));
|
||||
}
|
||||
|
||||
public function testRouter()
|
||||
{
|
||||
$container = $this->createContainerFromFile('full');
|
||||
|
2
src/Symfony/Component/Workflow/CHANGELOG.md
Normal file
2
src/Symfony/Component/Workflow/CHANGELOG.md
Normal file
@ -0,0 +1,2 @@
|
||||
CHANGELOG
|
||||
=========
|
110
src/Symfony/Component/Workflow/Definition.php
Normal file
110
src/Symfony/Component/Workflow/Definition.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Workflow\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Definition
|
||||
{
|
||||
private $places = array();
|
||||
|
||||
private $transitions = array();
|
||||
|
||||
private $initialPlace;
|
||||
|
||||
/**
|
||||
* Definition constructor.
|
||||
*
|
||||
* @param string[] $places
|
||||
* @param Transition[] $transitions
|
||||
*/
|
||||
public function __construct(array $places = array(), array $transitions = array())
|
||||
{
|
||||
$this->addPlaces($places);
|
||||
$this->addTransitions($transitions);
|
||||
}
|
||||
|
||||
public function getInitialPlace()
|
||||
{
|
||||
return $this->initialPlace;
|
||||
}
|
||||
|
||||
public function getPlaces()
|
||||
{
|
||||
return $this->places;
|
||||
}
|
||||
|
||||
public function getTransitions()
|
||||
{
|
||||
return $this->transitions;
|
||||
}
|
||||
|
||||
public function setInitialPlace($place)
|
||||
{
|
||||
if (!isset($this->places[$place])) {
|
||||
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
|
||||
}
|
||||
|
||||
$this->initialPlace = $place;
|
||||
}
|
||||
|
||||
public function addPlace($place)
|
||||
{
|
||||
if (!preg_match('{^[\w\d_-]+$}', $place)) {
|
||||
throw new InvalidArgumentException(sprintf('The place "%s" contains invalid characters.', $name));
|
||||
}
|
||||
|
||||
if (!count($this->places)) {
|
||||
$this->initialPlace = $place;
|
||||
}
|
||||
|
||||
$this->places[$place] = $place;
|
||||
}
|
||||
|
||||
public function addPlaces(array $places)
|
||||
{
|
||||
foreach ($places as $place) {
|
||||
$this->addPlace($place);
|
||||
}
|
||||
}
|
||||
|
||||
public function addTransitions(array $transitions)
|
||||
{
|
||||
foreach ($transitions as $transition) {
|
||||
$this->addTransition($transition);
|
||||
}
|
||||
}
|
||||
|
||||
public function addTransition(Transition $transition)
|
||||
{
|
||||
$name = $transition->getName();
|
||||
|
||||
foreach ($transition->getFroms() as $from) {
|
||||
if (!isset($this->places[$from])) {
|
||||
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($transition->getTos() as $to) {
|
||||
if (!isset($this->places[$to])) {
|
||||
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name));
|
||||
}
|
||||
}
|
||||
|
||||
$this->transitions[$name] = $transition;
|
||||
}
|
||||
}
|
35
src/Symfony/Component/Workflow/Dumper/DumperInterface.php
Normal file
35
src/Symfony/Component/Workflow/Dumper/DumperInterface.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* DumperInterface is the interface implemented by workflow dumper classes.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
interface DumperInterface
|
||||
{
|
||||
/**
|
||||
* Dumps a workflow definition.
|
||||
*
|
||||
* @param Definition $definition A Definition instance
|
||||
* @param Marking|null $marking A Marking instance
|
||||
* @param array $options An array of options
|
||||
*
|
||||
* @return string The representation of the workflow
|
||||
*/
|
||||
public function dump(Definition $definition, Marking $marking = null, array $options = array());
|
||||
}
|
205
src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
Normal file
205
src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* GraphvizDumper dumps a workflow as a graphviz file.
|
||||
*
|
||||
* You can convert the generated dot file with the dot utility (http://www.graphviz.org/):
|
||||
*
|
||||
* dot -Tpng workflow.dot > workflow.png
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class GraphvizDumper implements DumperInterface
|
||||
{
|
||||
private static $defaultOptions = array(
|
||||
'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'),
|
||||
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
|
||||
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
|
||||
);
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Dumps the workflow as a graphviz graph.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* * graph: The default options for the whole graph
|
||||
* * node: The default options for nodes (places + transitions)
|
||||
* * edge: The default options for edges
|
||||
*/
|
||||
public function dump(Definition $definition, Marking $marking = null, array $options = array())
|
||||
{
|
||||
$places = $this->findPlaces($definition, $marking);
|
||||
$transitions = $this->findTransitions($definition);
|
||||
$edges = $this->findEdges($definition);
|
||||
|
||||
$options = array_replace_recursive(self::$defaultOptions, $options);
|
||||
|
||||
return $this->startDot($options)
|
||||
.$this->addPlaces($places)
|
||||
.$this->addTransitions($transitions)
|
||||
.$this->addEdges($edges)
|
||||
.$this->endDot();
|
||||
}
|
||||
|
||||
private function findPlaces(Definition $definition, Marking $marking = null)
|
||||
{
|
||||
$places = array();
|
||||
|
||||
foreach ($definition->getPlaces() as $place) {
|
||||
$attributes = array();
|
||||
if ($place === $definition->getInitialPlace()) {
|
||||
$attributes['style'] = 'filled';
|
||||
}
|
||||
if ($marking && $marking->has($place)) {
|
||||
$attributes['color'] = '#FF0000';
|
||||
$attributes['shape'] = 'doublecircle';
|
||||
}
|
||||
$places[$place] = array(
|
||||
'attributes' => $attributes,
|
||||
);
|
||||
}
|
||||
|
||||
return $places;
|
||||
}
|
||||
|
||||
private function findTransitions(Definition $definition)
|
||||
{
|
||||
$transitions = array();
|
||||
|
||||
foreach ($definition->getTransitions() as $name => $transition) {
|
||||
$transitions[$name] = array(
|
||||
'attributes' => array('shape' => 'box', 'regular' => true),
|
||||
);
|
||||
}
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
private function addPlaces(array $places)
|
||||
{
|
||||
$code = '';
|
||||
|
||||
foreach ($places as $id => $place) {
|
||||
$code .= sprintf(" place_%s [label=\"%s\", shape=circle%s];\n",
|
||||
$this->dotize($id),
|
||||
$id,
|
||||
$this->addAttributes($place['attributes'])
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function addTransitions(array $transitions)
|
||||
{
|
||||
$code = '';
|
||||
|
||||
foreach ($transitions as $id => $place) {
|
||||
$code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n",
|
||||
$this->dotize($id),
|
||||
$id,
|
||||
$this->addAttributes($place['attributes'])
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function findEdges(Definition $definition)
|
||||
{
|
||||
$dotEdges = array();
|
||||
|
||||
foreach ($definition->getTransitions() as $transition) {
|
||||
foreach ($transition->getFroms() as $from) {
|
||||
$dotEdges[] = array(
|
||||
'from' => $from,
|
||||
'to' => $transition->getName(),
|
||||
'direction' => 'from',
|
||||
);
|
||||
}
|
||||
foreach ($transition->getTos() as $to) {
|
||||
$dotEdges[] = array(
|
||||
'from' => $transition->getName(),
|
||||
'to' => $to,
|
||||
'direction' => 'to',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $dotEdges;
|
||||
}
|
||||
|
||||
private function addEdges($edges)
|
||||
{
|
||||
$code = '';
|
||||
|
||||
foreach ($edges as $edge) {
|
||||
$code .= sprintf(" %s_%s -> %s_%s [style=\"solid\"];\n",
|
||||
'from' === $edge['direction'] ? 'place' : 'transition',
|
||||
$this->dotize($edge['from']),
|
||||
'from' === $edge['direction'] ? 'transition' : 'place',
|
||||
$this->dotize($edge['to'])
|
||||
);
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function startDot(array $options)
|
||||
{
|
||||
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
|
||||
$this->addOptions($options['graph']),
|
||||
$this->addOptions($options['node']),
|
||||
$this->addOptions($options['edge'])
|
||||
);
|
||||
}
|
||||
|
||||
private function endDot()
|
||||
{
|
||||
return "}\n";
|
||||
}
|
||||
|
||||
private function addAttributes($attributes)
|
||||
{
|
||||
$code = array();
|
||||
|
||||
foreach ($attributes as $k => $v) {
|
||||
$code[] = sprintf('%s="%s"', $k, $v);
|
||||
}
|
||||
|
||||
return $code ? ', '.implode(', ', $code) : '';
|
||||
}
|
||||
|
||||
private function addOptions($options)
|
||||
{
|
||||
$code = array();
|
||||
|
||||
foreach ($options as $k => $v) {
|
||||
$code[] = sprintf('%s="%s"', $k, $v);
|
||||
}
|
||||
|
||||
return implode(' ', $code);
|
||||
}
|
||||
|
||||
private function dotize($id)
|
||||
{
|
||||
return strtolower(preg_replace('/[^\w]/i', '_', $id));
|
||||
}
|
||||
}
|
58
src/Symfony/Component/Workflow/Event/Event.php
Normal file
58
src/Symfony/Component/Workflow/Event/Event.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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\Event;
|
||||
|
||||
use Symfony\Component\EventDispatcher\Event as BaseEvent;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Event extends BaseEvent
|
||||
{
|
||||
private $subject;
|
||||
|
||||
private $marking;
|
||||
|
||||
private $transition;
|
||||
|
||||
/**
|
||||
* Event constructor.
|
||||
*
|
||||
* @param mixed $subject
|
||||
* @param Marking $marking
|
||||
* @param Transition $transition
|
||||
*/
|
||||
public function __construct($subject, Marking $marking, Transition $transition)
|
||||
{
|
||||
$this->subject = $subject;
|
||||
$this->marking = $marking;
|
||||
$this->transition = $transition;
|
||||
}
|
||||
|
||||
public function getMarking()
|
||||
{
|
||||
return $this->marking;
|
||||
}
|
||||
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getTransition()
|
||||
{
|
||||
return $this->transition;
|
||||
}
|
||||
}
|
31
src/Symfony/Component/Workflow/Event/GuardEvent.php
Normal file
31
src/Symfony/Component/Workflow/Event/GuardEvent.php
Normal file
@ -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\Event;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class GuardEvent extends Event
|
||||
{
|
||||
private $blocked = false;
|
||||
|
||||
public function isBlocked()
|
||||
{
|
||||
return $this->blocked;
|
||||
}
|
||||
|
||||
public function setBlocked($blocked)
|
||||
{
|
||||
$this->blocked = (bool) $blocked;
|
||||
}
|
||||
}
|
30
src/Symfony/Component/Workflow/Event/TransitionEvent.php
Normal file
30
src/Symfony/Component/Workflow/Event/TransitionEvent.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class TransitionEvent extends Event
|
||||
{
|
||||
private $nextState;
|
||||
|
||||
public function setNextState($state)
|
||||
{
|
||||
$this->nextState = $state;
|
||||
}
|
||||
|
||||
public function getNextState()
|
||||
{
|
||||
return $this->nextState;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?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\EventListener;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class AuditTrailListener implements EventSubscriberInterface
|
||||
{
|
||||
private $logger;
|
||||
|
||||
public function __construct(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function onLeave(Event $event)
|
||||
{
|
||||
foreach ($event->getTransition()->getFroms() as $place) {
|
||||
$this->logger->info(sprintf('leaving "%s" for subject of class "%s"', $place, get_class($event->getSubject())));
|
||||
}
|
||||
}
|
||||
|
||||
public function onTransition(Event $event)
|
||||
{
|
||||
$this->logger->info(sprintf('transition "%s" for subject of class "%s"', $event->getTransition()->getName(), get_class($event->getSubject())));
|
||||
}
|
||||
|
||||
public function onEnter(Event $event)
|
||||
{
|
||||
foreach ($event->getTransition()->getTos() as $place) {
|
||||
$this->logger->info(sprintf('entering "%s" for subject of class "%s"', $place, get_class($event->getSubject())));
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return array(
|
||||
'workflow.leave' => array('onLeave'),
|
||||
'workflow.transition' => array('onTransition'),
|
||||
'workflow.enter' => array('onEnter'),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
20
src/Symfony/Component/Workflow/Exception/LogicException.php
Normal file
20
src/Symfony/Component/Workflow/Exception/LogicException.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
19
src/Symfony/Component/Workflow/LICENSE
Normal file
19
src/Symfony/Component/Workflow/LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014-2016 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
52
src/Symfony/Component/Workflow/Marking.php
Normal file
52
src/Symfony/Component/Workflow/Marking.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Marking contains the place of every tokens.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Marking
|
||||
{
|
||||
private $places = array();
|
||||
|
||||
/**
|
||||
* @param string[] $representation Keys are the place name and values should be 1
|
||||
*/
|
||||
public function __construct(array $representation = array())
|
||||
{
|
||||
foreach ($representation as $place => $nbToken) {
|
||||
$this->mark($place);
|
||||
}
|
||||
}
|
||||
|
||||
public function mark($place)
|
||||
{
|
||||
$this->places[$place] = 1;
|
||||
}
|
||||
|
||||
public function unmark($place)
|
||||
{
|
||||
unset($this->places[$place]);
|
||||
}
|
||||
|
||||
public function has($place)
|
||||
{
|
||||
return isset($this->places[$place]);
|
||||
}
|
||||
|
||||
public function getPlaces()
|
||||
{
|
||||
return $this->places;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* MarkingStoreInterface.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
interface MarkingStoreInterface
|
||||
{
|
||||
/**
|
||||
* Gets a Marking from a subject.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
*
|
||||
* @return Marking The marking
|
||||
*/
|
||||
public function getMarking($subject);
|
||||
|
||||
/**
|
||||
* Sets a Marking to a subject.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
* @param Marking $marking A marking
|
||||
*/
|
||||
public function setMarking($subject, Marking $marking);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* PropertyAccessorMarkingStore.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class PropertyAccessorMarkingStore implements MarkingStoreInterface
|
||||
{
|
||||
private $property;
|
||||
|
||||
private $propertyAccessor;
|
||||
|
||||
/**
|
||||
* PropertyAccessorMarkingStore constructor.
|
||||
*
|
||||
* @param string $property
|
||||
* @param PropertyAccessorInterface|null $propertyAccessor
|
||||
*/
|
||||
public function __construct($property = 'marking', PropertyAccessorInterface $propertyAccessor = null)
|
||||
{
|
||||
$this->property = $property;
|
||||
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMarking($subject)
|
||||
{
|
||||
return new Marking($this->propertyAccessor->getValue($subject, $this->property) ?: array());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setMarking($subject, Marking $marking)
|
||||
{
|
||||
$this->propertyAccessor->setValue($subject, $this->property, $marking->getPlaces());
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
/**
|
||||
* ScalarMarkingStore.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class ScalarMarkingStore implements MarkingStoreInterface, UniqueTransitionOutputInterface
|
||||
{
|
||||
private $property;
|
||||
|
||||
private $propertyAccessor;
|
||||
|
||||
/**
|
||||
* ScalarMarkingStore constructor.
|
||||
*
|
||||
* @param string $property
|
||||
* @param PropertyAccessorInterface|null $propertyAccessor
|
||||
*/
|
||||
public function __construct($property = 'marking', PropertyAccessorInterface $propertyAccessor = null)
|
||||
{
|
||||
$this->property = $property;
|
||||
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMarking($subject)
|
||||
{
|
||||
$placeName = $this->propertyAccessor->getValue($subject, $this->property);
|
||||
|
||||
if (!$placeName) {
|
||||
return new Marking();
|
||||
}
|
||||
|
||||
return new Marking(array($placeName => 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setMarking($subject, Marking $marking)
|
||||
{
|
||||
$this->propertyAccessor->setValue($subject, $this->property, key($marking->getPlaces()));
|
||||
}
|
||||
}
|
@ -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\MarkingStore;
|
||||
|
||||
/**
|
||||
* UniqueTransitionOutputInterface.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
interface UniqueTransitionOutputInterface
|
||||
{
|
||||
}
|
11
src/Symfony/Component/Workflow/README.md
Normal file
11
src/Symfony/Component/Workflow/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
Workflow Component
|
||||
===================
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/workflow/introduction.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
65
src/Symfony/Component/Workflow/Registry.php
Normal file
65
src/Symfony/Component/Workflow/Registry.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Registry
|
||||
{
|
||||
private $workflows = array();
|
||||
|
||||
/**
|
||||
* @param Workflow $workflow
|
||||
* @param string $classname
|
||||
*/
|
||||
public function add(Workflow $workflow, $classname)
|
||||
{
|
||||
$this->workflows[] = array($workflow, $classname);
|
||||
}
|
||||
|
||||
public function get($subject, $workflowName = null)
|
||||
{
|
||||
$matched = null;
|
||||
|
||||
foreach ($this->workflows as list($workflow, $classname)) {
|
||||
if ($this->supports($workflow, $classname, $subject, $workflowName)) {
|
||||
if ($matched) {
|
||||
throw new InvalidArgumentException('At least two workflows match this subject. Set a different name on each and use the second (name) argument of this method.');
|
||||
}
|
||||
$matched = $workflow;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$matched) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($subject)));
|
||||
}
|
||||
|
||||
return $matched;
|
||||
}
|
||||
|
||||
private function supports(Workflow $workflow, $classname, $subject, $name)
|
||||
{
|
||||
if (!$subject instanceof $classname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $name === $workflow->getName();
|
||||
}
|
||||
}
|
73
src/Symfony/Component/Workflow/Tests/DefinitionTest.php
Normal file
73
src/Symfony/Component/Workflow/Tests/DefinitionTest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests;
|
||||
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class DefinitionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testAddPlaces()
|
||||
{
|
||||
$places = range('a', 'e');
|
||||
$definition = new Definition($places);
|
||||
|
||||
$this->assertCount(5, $definition->getPlaces());
|
||||
|
||||
$this->assertEquals('a', $definition->getInitialPlace());
|
||||
}
|
||||
|
||||
public function testSetInitialPlace()
|
||||
{
|
||||
$places = range('a', 'e');
|
||||
$definition = new Definition($places);
|
||||
|
||||
$definition->setInitialPlace($places[3]);
|
||||
|
||||
$this->assertEquals($places[3], $definition->getInitialPlace());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Place "d" cannot be the initial place as it does not exist.
|
||||
*/
|
||||
public function testSetInitialPlaceAndPlaceIsNotDefined()
|
||||
{
|
||||
$definition = new Definition();
|
||||
|
||||
$definition->setInitialPlace('d');
|
||||
}
|
||||
|
||||
public function testAddTransition()
|
||||
{
|
||||
$places = range('a', 'b');
|
||||
|
||||
$transition = new Transition('name', $places[0], $places[1]);
|
||||
$definition = new Definition($places, array($transition));
|
||||
|
||||
$this->assertCount(1, $definition->getTransitions());
|
||||
$this->assertSame($transition, $definition->getTransitions()['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Place "c" referenced in transition "name" does not exist.
|
||||
*/
|
||||
public function testAddTransitionAndFromPlaceIsNotDefined()
|
||||
{
|
||||
$places = range('a', 'b');
|
||||
|
||||
new Definition($places, array(new Transition('name', 'c', $places[1])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Place "c" referenced in transition "name" does not exist.
|
||||
*/
|
||||
public function testAddTransitionAndToPlaceIsNotDefined()
|
||||
{
|
||||
$places = range('a', 'b');
|
||||
|
||||
new Definition($places, array(new Transition('name', $places[0], 'c')));
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests\Dumper;
|
||||
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class GraphvizDumperTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $dumper;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->dumper = new GraphvizDumper();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideWorkflowDefinitionWithoutMarking
|
||||
*/
|
||||
public function testGraphvizDumperWithoutMarking($definition, $expected)
|
||||
{
|
||||
$dump = $this->dumper->dump($definition);
|
||||
|
||||
$this->assertEquals($expected, $dump);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideWorkflowDefinitionWithMarking
|
||||
*/
|
||||
public function testWorkflowWithMarking($definition, $marking, $expected)
|
||||
{
|
||||
$dump = $this->dumper->dump($definition, $marking);
|
||||
|
||||
$this->assertEquals($expected, $dump);
|
||||
}
|
||||
|
||||
public function provideWorkflowDefinitionWithMarking()
|
||||
{
|
||||
yield array(
|
||||
$this->createprovideComplexWorkflowDefinition(),
|
||||
new Marking(array('b' => 1)),
|
||||
$this->createComplexWorkflowDumpWithMarking(),
|
||||
);
|
||||
|
||||
yield array(
|
||||
$this->provideSimpleWorkflowDefinition(),
|
||||
new Marking(array('c' => 1, 'd' => 1)),
|
||||
$this->createSimpleWorkflowDumpWithMarking(),
|
||||
);
|
||||
}
|
||||
|
||||
public function provideWorkflowDefinitionWithoutMarking()
|
||||
{
|
||||
yield array($this->createprovideComplexWorkflowDefinition(), $this->provideComplexWorkflowDumpWithoutMarking());
|
||||
yield array($this->provideSimpleWorkflowDefinition(), $this->provideSimpleWorkflowDumpWithoutMarking());
|
||||
}
|
||||
|
||||
public function createprovideComplexWorkflowDefinition()
|
||||
{
|
||||
$definition = new Definition();
|
||||
|
||||
$definition->addPlaces(range('a', 'g'));
|
||||
|
||||
$definition->addTransition(new Transition('t1', 'a', array('b', 'c')));
|
||||
$definition->addTransition(new Transition('t2', array('b', 'c'), 'd'));
|
||||
$definition->addTransition(new Transition('t3', 'd', 'e'));
|
||||
$definition->addTransition(new Transition('t4', 'd', 'f'));
|
||||
$definition->addTransition(new Transition('t5', 'e', 'g'));
|
||||
$definition->addTransition(new Transition('t6', 'f', 'g'));
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
public function provideSimpleWorkflowDefinition()
|
||||
{
|
||||
$definition = new Definition();
|
||||
|
||||
$definition->addPlaces(range('a', 'c'));
|
||||
|
||||
$definition->addTransition(new Transition('t1', 'a', 'b'));
|
||||
$definition->addTransition(new Transition('t2', 'b', 'c'));
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
public function createComplexWorkflowDumpWithMarking()
|
||||
{
|
||||
return 'digraph workflow {
|
||||
ratio="compress" rankdir="LR"
|
||||
node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"];
|
||||
edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"];
|
||||
|
||||
place_a [label="a", shape=circle, style="filled"];
|
||||
place_b [label="b", shape=circle, color="#FF0000", shape="doublecircle"];
|
||||
place_c [label="c", shape=circle];
|
||||
place_d [label="d", shape=circle];
|
||||
place_e [label="e", shape=circle];
|
||||
place_f [label="f", shape=circle];
|
||||
place_g [label="g", shape=circle];
|
||||
transition_t1 [label="t1", shape=box, shape="box", regular="1"];
|
||||
transition_t2 [label="t2", shape=box, shape="box", regular="1"];
|
||||
transition_t3 [label="t3", shape=box, shape="box", regular="1"];
|
||||
transition_t4 [label="t4", shape=box, shape="box", regular="1"];
|
||||
transition_t5 [label="t5", shape=box, shape="box", regular="1"];
|
||||
transition_t6 [label="t6", shape=box, shape="box", regular="1"];
|
||||
place_a -> transition_t1 [style="solid"];
|
||||
transition_t1 -> place_b [style="solid"];
|
||||
transition_t1 -> place_c [style="solid"];
|
||||
place_b -> transition_t2 [style="solid"];
|
||||
place_c -> transition_t2 [style="solid"];
|
||||
transition_t2 -> place_d [style="solid"];
|
||||
place_d -> transition_t3 [style="solid"];
|
||||
transition_t3 -> place_e [style="solid"];
|
||||
place_d -> transition_t4 [style="solid"];
|
||||
transition_t4 -> place_f [style="solid"];
|
||||
place_e -> transition_t5 [style="solid"];
|
||||
transition_t5 -> place_g [style="solid"];
|
||||
place_f -> transition_t6 [style="solid"];
|
||||
transition_t6 -> place_g [style="solid"];
|
||||
}
|
||||
';
|
||||
}
|
||||
|
||||
public function createSimpleWorkflowDumpWithMarking()
|
||||
{
|
||||
return 'digraph workflow {
|
||||
ratio="compress" rankdir="LR"
|
||||
node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"];
|
||||
edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"];
|
||||
|
||||
place_a [label="a", shape=circle, style="filled"];
|
||||
place_b [label="b", shape=circle];
|
||||
place_c [label="c", shape=circle, color="#FF0000", shape="doublecircle"];
|
||||
transition_t1 [label="t1", shape=box, shape="box", regular="1"];
|
||||
transition_t2 [label="t2", shape=box, shape="box", regular="1"];
|
||||
place_a -> transition_t1 [style="solid"];
|
||||
transition_t1 -> place_b [style="solid"];
|
||||
place_b -> transition_t2 [style="solid"];
|
||||
transition_t2 -> place_c [style="solid"];
|
||||
}
|
||||
';
|
||||
}
|
||||
|
||||
public function provideComplexWorkflowDumpWithoutMarking()
|
||||
{
|
||||
return 'digraph workflow {
|
||||
ratio="compress" rankdir="LR"
|
||||
node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"];
|
||||
edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"];
|
||||
|
||||
place_a [label="a", shape=circle, style="filled"];
|
||||
place_b [label="b", shape=circle];
|
||||
place_c [label="c", shape=circle];
|
||||
place_d [label="d", shape=circle];
|
||||
place_e [label="e", shape=circle];
|
||||
place_f [label="f", shape=circle];
|
||||
place_g [label="g", shape=circle];
|
||||
transition_t1 [label="t1", shape=box, shape="box", regular="1"];
|
||||
transition_t2 [label="t2", shape=box, shape="box", regular="1"];
|
||||
transition_t3 [label="t3", shape=box, shape="box", regular="1"];
|
||||
transition_t4 [label="t4", shape=box, shape="box", regular="1"];
|
||||
transition_t5 [label="t5", shape=box, shape="box", regular="1"];
|
||||
transition_t6 [label="t6", shape=box, shape="box", regular="1"];
|
||||
place_a -> transition_t1 [style="solid"];
|
||||
transition_t1 -> place_b [style="solid"];
|
||||
transition_t1 -> place_c [style="solid"];
|
||||
place_b -> transition_t2 [style="solid"];
|
||||
place_c -> transition_t2 [style="solid"];
|
||||
transition_t2 -> place_d [style="solid"];
|
||||
place_d -> transition_t3 [style="solid"];
|
||||
transition_t3 -> place_e [style="solid"];
|
||||
place_d -> transition_t4 [style="solid"];
|
||||
transition_t4 -> place_f [style="solid"];
|
||||
place_e -> transition_t5 [style="solid"];
|
||||
transition_t5 -> place_g [style="solid"];
|
||||
place_f -> transition_t6 [style="solid"];
|
||||
transition_t6 -> place_g [style="solid"];
|
||||
}
|
||||
';
|
||||
}
|
||||
|
||||
public function provideSimpleWorkflowDumpWithoutMarking()
|
||||
{
|
||||
return 'digraph workflow {
|
||||
ratio="compress" rankdir="LR"
|
||||
node [fontsize="9" fontname="Arial" color="#333333" fillcolor="lightblue" fixedsize="1" width="1"];
|
||||
edge [fontsize="9" fontname="Arial" color="#333333" arrowhead="normal" arrowsize="0.5"];
|
||||
|
||||
place_a [label="a", shape=circle, style="filled"];
|
||||
place_b [label="b", shape=circle];
|
||||
place_c [label="c", shape=circle];
|
||||
transition_t1 [label="t1", shape=box, shape="box", regular="1"];
|
||||
transition_t2 [label="t2", shape=box, shape="box", regular="1"];
|
||||
place_a -> transition_t1 [style="solid"];
|
||||
transition_t1 -> place_b [style="solid"];
|
||||
place_b -> transition_t2 [style="solid"];
|
||||
transition_t2 -> place_c [style="solid"];
|
||||
}
|
||||
';
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests\EventListener;
|
||||
|
||||
use Psr\Log\AbstractLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\EventListener\AuditTrailListener;
|
||||
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
|
||||
class AuditTrailListenerTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testItWorks()
|
||||
{
|
||||
$transitions = array(
|
||||
new Transition('t1', 'a', 'b'),
|
||||
new Transition('t2', 'a', 'b'),
|
||||
);
|
||||
|
||||
$definition = new Definition(array('a', 'b'), $transitions);
|
||||
|
||||
$object = new \stdClass();
|
||||
$object->marking = null;
|
||||
|
||||
$logger = new Logger();
|
||||
|
||||
$ed = new EventDispatcher();
|
||||
$ed->addSubscriber(new AuditTrailListener($logger));
|
||||
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore(), $ed);
|
||||
|
||||
$workflow->apply($object, 't1');
|
||||
|
||||
$expected = array(
|
||||
'leaving "a" for subject of class "stdClass"',
|
||||
'transition "t1" for subject of class "stdClass"',
|
||||
'entering "b" for subject of class "stdClass"',
|
||||
);
|
||||
|
||||
$this->assertSame($expected, $logger->logs);
|
||||
}
|
||||
}
|
||||
|
||||
class Logger extends AbstractLogger
|
||||
{
|
||||
public $logs = array();
|
||||
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
$this->logs[] = $message;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests\MarkingStore;
|
||||
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
|
||||
|
||||
class PropertyAccessorMarkingStoreTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testGetSetMarking()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->myMarks = null;
|
||||
|
||||
$markingStore = new PropertyAccessorMarkingStore('myMarks');
|
||||
|
||||
$marking = $markingStore->getMarking($subject);
|
||||
|
||||
$this->assertInstanceOf(Marking::class, $marking);
|
||||
$this->assertCount(0, $marking->getPlaces());
|
||||
|
||||
$marking->mark('first_place');
|
||||
|
||||
$markingStore->setMarking($subject, $marking);
|
||||
|
||||
$this->assertSame(array('first_place' => 1), $subject->myMarks);
|
||||
|
||||
$marking2 = $markingStore->getMarking($subject);
|
||||
|
||||
$this->assertEquals($marking, $marking2);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests\MarkingStore;
|
||||
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
|
||||
|
||||
class ScalarMarkingStoreTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testGetSetMarking()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->myMarks = null;
|
||||
|
||||
$markingStore = new ScalarMarkingStore('myMarks');
|
||||
|
||||
$marking = $markingStore->getMarking($subject);
|
||||
|
||||
$this->assertInstanceOf(Marking::class, $marking);
|
||||
$this->assertCount(0, $marking->getPlaces());
|
||||
|
||||
$marking->mark('first_place');
|
||||
|
||||
$markingStore->setMarking($subject, $marking);
|
||||
|
||||
$this->assertSame('first_place', $subject->myMarks);
|
||||
|
||||
$marking2 = $markingStore->getMarking($subject);
|
||||
|
||||
$this->assertEquals($marking, $marking2);
|
||||
}
|
||||
}
|
35
src/Symfony/Component/Workflow/Tests/MarkingTest.php
Normal file
35
src/Symfony/Component/Workflow/Tests/MarkingTest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests;
|
||||
|
||||
use Symfony\Component\Workflow\Marking;
|
||||
|
||||
class MarkingTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testMarking()
|
||||
{
|
||||
$marking = new Marking(array('a' => 1));
|
||||
|
||||
$this->assertTrue($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertSame(array('a' => 1), $marking->getPlaces());
|
||||
|
||||
$marking->mark('b');
|
||||
|
||||
$this->assertTrue($marking->has('a'));
|
||||
$this->assertTrue($marking->has('b'));
|
||||
$this->assertSame(array('a' => 1, 'b' => 1), $marking->getPlaces());
|
||||
|
||||
$marking->unmark('a');
|
||||
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertTrue($marking->has('b'));
|
||||
$this->assertSame(array('b' => 1), $marking->getPlaces());
|
||||
|
||||
$marking->unmark('b');
|
||||
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertFalse($marking->has('b'));
|
||||
$this->assertSame(array(), $marking->getPlaces());
|
||||
}
|
||||
}
|
74
src/Symfony/Component/Workflow/Tests/RegistryTest.php
Normal file
74
src/Symfony/Component/Workflow/Tests/RegistryTest.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Workflow;
|
||||
|
||||
class RegistryTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
private $registry;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$workflows = array();
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
$this->registry = null;
|
||||
}
|
||||
|
||||
public function testGetWithSuccess()
|
||||
{
|
||||
$workflow = $this->registry->get(new Subject1());
|
||||
$this->assertInstanceOf(Workflow::class, $workflow);
|
||||
$this->assertSame('workflow1', $workflow->getName());
|
||||
|
||||
$workflow = $this->registry->get(new Subject1(), 'workflow1');
|
||||
$this->assertInstanceOf(Workflow::class, $workflow);
|
||||
$this->assertSame('workflow1', $workflow->getName());
|
||||
|
||||
$workflow = $this->registry->get(new Subject2(), 'workflow2');
|
||||
$this->assertInstanceOf(Workflow::class, $workflow);
|
||||
$this->assertSame('workflow2', $workflow->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage At least two workflows match this subject. Set a different name on each and use the second (name) argument of this method.
|
||||
*/
|
||||
public function testGetWithMultipleMatch()
|
||||
{
|
||||
$w1 = $this->registry->get(new Subject2());
|
||||
$this->assertInstanceOf(Workflow::class, $w1);
|
||||
$this->assertSame('workflow1', $w1->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage Unable to find a workflow for class "stdClass".
|
||||
*/
|
||||
public function testGetWithNoMatch()
|
||||
{
|
||||
$w1 = $this->registry->get(new \stdClass());
|
||||
$this->assertInstanceOf(Workflow::class, $w1);
|
||||
$this->assertSame('workflow1', $w1->getName());
|
||||
}
|
||||
}
|
||||
|
||||
class Subject1
|
||||
{
|
||||
}
|
||||
class Subject2
|
||||
{
|
||||
}
|
26
src/Symfony/Component/Workflow/Tests/TransitionTest.php
Normal file
26
src/Symfony/Component/Workflow/Tests/TransitionTest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests;
|
||||
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class TransitionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage The transition "foo.bar" contains invalid characters.
|
||||
*/
|
||||
public function testValidateName()
|
||||
{
|
||||
$transition = new Transition('foo.bar', 'a', 'b');
|
||||
}
|
||||
|
||||
public function testConstructor()
|
||||
{
|
||||
$transition = new Transition('name', 'a', 'b');
|
||||
|
||||
$this->assertSame('name', $transition->getName());
|
||||
$this->assertSame(array('a'), $transition->getFroms());
|
||||
$this->assertSame(array('b'), $transition->getTos());
|
||||
}
|
||||
}
|
288
src/Symfony/Component/Workflow/Tests/WorkflowTest.php
Normal file
288
src/Symfony/Component/Workflow/Tests/WorkflowTest.php
Normal file
@ -0,0 +1,288 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Workflow\Tests;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Workflow\Definition;
|
||||
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".
|
||||
*/
|
||||
public function testGetMarkingWithInvalidStoreReturn()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow(new Definition(), $this->getMock(MarkingStoreInterface::class));
|
||||
|
||||
$workflow->getMarking($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage The Marking is empty and there is no initial place for workflow "unnamed".
|
||||
*/
|
||||
public function testGetMarkingWithEmptyDefinition()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow(new Definition(), new PropertyAccessorMarkingStore());
|
||||
|
||||
$workflow->getMarking($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Place "nope" is not valid for workflow "unnamed".
|
||||
*/
|
||||
public function testGetMarkingWithImpossiblePlace()
|
||||
{
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$subject->marking = array('nope' => true);
|
||||
$workflow = new Workflow(new Definition(), new PropertyAccessorMarkingStore());
|
||||
|
||||
$workflow->getMarking($subject);
|
||||
}
|
||||
|
||||
public function testGetMarkingWithEmptyInitialMarking()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$marking = $workflow->getMarking($subject);
|
||||
|
||||
$this->assertInstanceOf(Marking::class, $marking);
|
||||
$this->assertTrue($marking->has('a'));
|
||||
$this->assertSame(array('a' => 1), $subject->marking);
|
||||
}
|
||||
|
||||
public function testGetMarkingWithExistingMarking()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$subject->marking = array('b' => 1, 'c' => 1);
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$marking = $workflow->getMarking($subject);
|
||||
|
||||
$this->assertInstanceOf(Marking::class, $marking);
|
||||
$this->assertTrue($marking->has('b'));
|
||||
$this->assertTrue($marking->has('c'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Transition "foobar" does not exist for workflow "unnamed".
|
||||
*/
|
||||
public function testCanWithUnexistingTransition()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$workflow->can($subject, 'foobar');
|
||||
}
|
||||
|
||||
public function testCan()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$this->assertTrue($workflow->can($subject, 't1'));
|
||||
$this->assertFalse($workflow->can($subject, 't2'));
|
||||
}
|
||||
|
||||
public function testCanWithGuard()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Symfony\Component\Workflow\Exception\LogicException
|
||||
* @expectedExceptionMessage Unable to apply transition "t2" for workflow "unnamed".
|
||||
*/
|
||||
public function testApplyWithImpossibleTransition()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$workflow->apply($subject, 't2');
|
||||
}
|
||||
|
||||
public function testApply()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore());
|
||||
|
||||
$marking = $workflow->apply($subject, 't1');
|
||||
|
||||
$this->assertInstanceOf(Marking::class, $marking);
|
||||
$this->assertFalse($marking->has('a'));
|
||||
$this->assertTrue($marking->has('b'));
|
||||
$this->assertTrue($marking->has('c'));
|
||||
}
|
||||
|
||||
public function testApplyWithEventDispatcher()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$eventDispatcher = new EventDispatcherMock();
|
||||
$workflow = new Workflow($definition, new PropertyAccessorMarkingStore(), $eventDispatcher, 'workflow_name');
|
||||
|
||||
$eventNameExpected = array(
|
||||
'workflow.guard',
|
||||
'workflow.workflow_name.guard',
|
||||
'workflow.workflow_name.guard.t1',
|
||||
'workflow.leave',
|
||||
'workflow.workflow_name.leave',
|
||||
'workflow.workflow_name.leave.a',
|
||||
'workflow.transition',
|
||||
'workflow.workflow_name.transition',
|
||||
'workflow.workflow_name.transition.t1',
|
||||
'workflow.enter',
|
||||
'workflow.workflow_name.enter',
|
||||
'workflow.workflow_name.enter.b',
|
||||
'workflow.workflow_name.enter.c',
|
||||
// Following events are fired because of announce() method
|
||||
'workflow.guard',
|
||||
'workflow.workflow_name.guard',
|
||||
'workflow.workflow_name.guard.t2',
|
||||
'workflow.workflow_name.announce.t2',
|
||||
);
|
||||
|
||||
$marking = $workflow->apply($subject, 't1');
|
||||
|
||||
$this->assertSame($eventNameExpected, $eventDispatcher->dispatchedEvents);
|
||||
}
|
||||
|
||||
public function testGetEnabledTransitions()
|
||||
{
|
||||
$definition = $this->createComplexWorkflow();
|
||||
$subject = new \stdClass();
|
||||
$subject->marking = null;
|
||||
$eventDispatcher = new EventDispatcher();
|
||||
$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));
|
||||
|
||||
$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());
|
||||
|
||||
$subject->marking = array('c' => true, 'e' => true);
|
||||
$transitions = $workflow->getEnabledTransitions($subject);
|
||||
$this->assertCount(1, $transitions);
|
||||
$this->assertSame('t5', $transitions['t5']->getName());
|
||||
}
|
||||
|
||||
private function createComplexWorkflow()
|
||||
{
|
||||
$definition = new Definition();
|
||||
|
||||
$definition->addPlaces(range('a', 'g'));
|
||||
|
||||
$definition->addTransition(new Transition('t1', 'a', array('b', 'c')));
|
||||
$definition->addTransition(new Transition('t2', array('b', 'c'), 'd'));
|
||||
$definition->addTransition(new Transition('t3', 'd', 'e'));
|
||||
$definition->addTransition(new Transition('t4', 'd', 'f'));
|
||||
$definition->addTransition(new Transition('t5', 'e', 'g'));
|
||||
$definition->addTransition(new Transition('t6', 'f', 'g'));
|
||||
|
||||
return $definition;
|
||||
|
||||
// The graph looks like:
|
||||
//
|
||||
// +---+ +----+ +---+ +----+ +----+ +----+ +----+ +----+ +---+
|
||||
// | a | --> | t1 | --> | c | --> | t2 | --> | d | --> | t4 | --> | f | --> | t6 | --> | g |
|
||||
// +---+ +----+ +---+ +----+ +----+ +----+ +----+ +----+ +---+
|
||||
// | ^ | ^
|
||||
// | | | |
|
||||
// v | v |
|
||||
// +----+ | +----+ +----+ +----+ |
|
||||
// | b | ----------------+ | t3 | --> | e | --> | t5 | -----------------+
|
||||
// +----+ +----+ +----+ +----+
|
||||
}
|
||||
}
|
||||
|
||||
class EventDispatcherMock implements \Symfony\Component\EventDispatcher\EventDispatcherInterface
|
||||
{
|
||||
public $dispatchedEvents = array();
|
||||
|
||||
public function dispatch($eventName, \Symfony\Component\EventDispatcher\Event $event = null)
|
||||
{
|
||||
$this->dispatchedEvents[] = $eventName;
|
||||
}
|
||||
|
||||
public function addListener($eventName, $listener, $priority = 0)
|
||||
{
|
||||
}
|
||||
public function addSubscriber(\Symfony\Component\EventDispatcher\EventSubscriberInterface $subscriber)
|
||||
{
|
||||
}
|
||||
public function removeListener($eventName, $listener)
|
||||
{
|
||||
}
|
||||
public function removeSubscriber(\Symfony\Component\EventDispatcher\EventSubscriberInterface $subscriber)
|
||||
{
|
||||
}
|
||||
public function getListeners($eventName = null)
|
||||
{
|
||||
}
|
||||
public function getListenerPriority($eventName, $listener)
|
||||
{
|
||||
}
|
||||
public function hasListeners($eventName = null)
|
||||
{
|
||||
}
|
||||
}
|
60
src/Symfony/Component/Workflow/Transition.php
Normal file
60
src/Symfony/Component/Workflow/Transition.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Transition
|
||||
{
|
||||
private $name;
|
||||
|
||||
private $froms;
|
||||
|
||||
private $tos;
|
||||
|
||||
/**
|
||||
* Transition constructor.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|string[] $froms
|
||||
* @param string|string[] $tos
|
||||
*/
|
||||
public function __construct($name, $froms, $tos)
|
||||
{
|
||||
if (!preg_match('{^[\w\d_-]+$}', $name)) {
|
||||
throw new InvalidArgumentException(sprintf('The transition "%s" contains invalid characters.', $name));
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->froms = (array) $froms;
|
||||
$this->tos = (array) $tos;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getFroms()
|
||||
{
|
||||
return $this->froms;
|
||||
}
|
||||
|
||||
public function getTos()
|
||||
{
|
||||
return $this->tos;
|
||||
}
|
||||
}
|
274
src/Symfony/Component/Workflow/Workflow.php
Normal file
274
src/Symfony/Component/Workflow/Workflow.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class Workflow
|
||||
{
|
||||
private $definition;
|
||||
private $markingStore;
|
||||
private $dispatcher;
|
||||
private $name;
|
||||
|
||||
public function __construct(Definition $definition, MarkingStoreInterface $markingStore, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
|
||||
{
|
||||
$this->definition = $definition;
|
||||
$this->markingStore = $markingStore;
|
||||
$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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object's Marking.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
*
|
||||
* @return Marking The Marking
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function getMarking($subject)
|
||||
{
|
||||
$marking = $this->markingStore->getMarking($subject);
|
||||
|
||||
if (!$marking instanceof Marking) {
|
||||
throw new LogicException(sprintf('The value returned by the MarkingStore is not an instance of "%s" for workflow "%s".', Marking::class, $this->name));
|
||||
}
|
||||
|
||||
// check if the subject is already in the workflow
|
||||
if (!$marking->getPlaces()) {
|
||||
if (!$this->definition->getInitialPlace()) {
|
||||
throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name));
|
||||
}
|
||||
$marking->mark($this->definition->getInitialPlace());
|
||||
}
|
||||
|
||||
// check that the subject has a known place
|
||||
$places = $this->definition->getPlaces();
|
||||
foreach ($marking->getPlaces() as $placeName => $nbToken) {
|
||||
if (!isset($places[$placeName])) {
|
||||
$message = sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name);
|
||||
if (!$places) {
|
||||
$message .= ' It seems you forgot to add places to the current workflow.';
|
||||
}
|
||||
|
||||
throw new LogicException($message);
|
||||
}
|
||||
}
|
||||
|
||||
// Because the marking could have been initialized, we update the subject
|
||||
$this->markingStore->setMarking($subject, $marking);
|
||||
|
||||
return $marking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the transition is enabled.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
* @param string $transitionName A transition
|
||||
*
|
||||
* @return bool true if the transition is enabled
|
||||
*
|
||||
* @throws LogicException If the transition does not exist
|
||||
*/
|
||||
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];
|
||||
|
||||
$marking = $this->getMarking($subject);
|
||||
|
||||
return $this->doCan($subject, $marking, $transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a transition.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
* @param string $transitionName A transition
|
||||
*
|
||||
* @return Marking The new Marking
|
||||
*
|
||||
* @throws LogicException If the transition is not applicable
|
||||
* @throws LogicException If the transition does not exist
|
||||
*/
|
||||
public function apply($subject, $transitionName)
|
||||
{
|
||||
if (!$this->can($subject, $transitionName)) {
|
||||
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);
|
||||
|
||||
$this->enter($subject, $transition, $marking);
|
||||
|
||||
$this->announce($subject, $transition, $marking);
|
||||
|
||||
$this->markingStore->setMarking($subject, $marking);
|
||||
|
||||
return $marking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all enabled transitions.
|
||||
*
|
||||
* @param object $subject A subject
|
||||
*
|
||||
* @return Transition[] All enabled transitions
|
||||
*/
|
||||
public function getEnabledTransitions($subject)
|
||||
{
|
||||
$enabled = array();
|
||||
|
||||
$marking = $this->getMarking($subject);
|
||||
|
||||
foreach ($this->definition->getTransitions() as $transition) {
|
||||
if ($this->doCan($subject, $marking, $transition)) {
|
||||
$enabled[$transition->getName()] = $transition;
|
||||
}
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private function guardTransition($subject, Marking $marking, Transition $transition)
|
||||
{
|
||||
if (null === $this->dispatcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new GuardEvent($subject, $marking, $transition);
|
||||
|
||||
$this->dispatcher->dispatch('workflow.guard', $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.guard', $this->name), $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.guard.%s', $this->name, $transition->getName()), $event);
|
||||
|
||||
return $event->isBlocked();
|
||||
}
|
||||
|
||||
private function leave($subject, Transition $transition, Marking $marking)
|
||||
{
|
||||
if (null !== $this->dispatcher) {
|
||||
$event = new Event($subject, $marking, $transition);
|
||||
|
||||
$this->dispatcher->dispatch('workflow.leave', $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), $event);
|
||||
}
|
||||
|
||||
foreach ($transition->getFroms() as $place) {
|
||||
$marking->unmark($place);
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $place), $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function transition($subject, Transition $transition, Marking $marking)
|
||||
{
|
||||
if (null === $this->dispatcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new Event($subject, $marking, $transition);
|
||||
|
||||
$this->dispatcher->dispatch('workflow.transition', $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.transition', $this->name), $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()), $event);
|
||||
}
|
||||
|
||||
private function enter($subject, Transition $transition, Marking $marking)
|
||||
{
|
||||
if (null !== $this->dispatcher) {
|
||||
$event = new Event($subject, $marking, $transition);
|
||||
|
||||
$this->dispatcher->dispatch('workflow.enter', $event);
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.enter', $this->name), $event);
|
||||
}
|
||||
|
||||
foreach ($transition->getTos() as $place) {
|
||||
$marking->mark($place);
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.enter.%s', $this->name, $place), $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function announce($subject, Transition $initialTransition, Marking $marking)
|
||||
{
|
||||
if (null === $this->dispatcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new Event($subject, $marking, $initialTransition);
|
||||
|
||||
foreach ($this->definition->getTransitions() as $transition) {
|
||||
if ($this->doCan($subject, $marking, $transition)) {
|
||||
$this->dispatcher->dispatch(sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()), $event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
src/Symfony/Component/Workflow/composer.json
Normal file
40
src/Symfony/Component/Workflow/composer.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "symfony/workflow",
|
||||
"type": "library",
|
||||
"description": "Symfony Workflow Component",
|
||||
"keywords": ["workflow", "petrinet", "place", "transition"],
|
||||
"homepage": "http://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Grégoire Pineau",
|
||||
"email": "lyrixx@lyrixx.info"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"psr/log": "~1.0",
|
||||
"symfony/event-dispatcher": "~2.1|~3.0",
|
||||
"symfony/property-access": "~2.3|~3.0",
|
||||
"twig/twig": "~1.14"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Workflow\\": "" }
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.2-dev"
|
||||
}
|
||||
}
|
||||
}
|
28
src/Symfony/Component/Workflow/phpunit.xml.dist
Normal file
28
src/Symfony/Component/Workflow/phpunit.xml.dist
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony Workflow Component Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
Reference in New Issue
Block a user