[Workflow] Added initial set of files
This commit is contained in:
parent
17d59a7c66
commit
078e27f139
@ -72,6 +72,7 @@
|
|||||||
"symfony/validator": "self.version",
|
"symfony/validator": "self.version",
|
||||||
"symfony/var-dumper": "self.version",
|
"symfony/var-dumper": "self.version",
|
||||||
"symfony/web-profiler-bundle": "self.version",
|
"symfony/web-profiler-bundle": "self.version",
|
||||||
|
"symfony/workflow": "self.version",
|
||||||
"symfony/yaml": "self.version"
|
"symfony/yaml": "self.version"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"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->addSsiSection($rootNode);
|
||||||
$this->addFragmentsSection($rootNode);
|
$this->addFragmentsSection($rootNode);
|
||||||
$this->addProfilerSection($rootNode);
|
$this->addProfilerSection($rootNode);
|
||||||
|
$this->addWorkflowSection($rootNode);
|
||||||
$this->addRouterSection($rootNode);
|
$this->addRouterSection($rootNode);
|
||||||
$this->addSessionSection($rootNode);
|
$this->addSessionSection($rootNode);
|
||||||
$this->addRequestSection($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)
|
private function addRouterSection(ArrayNodeDefinition $rootNode)
|
||||||
{
|
{
|
||||||
$rootNode
|
$rootNode
|
||||||
|
@ -31,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
|
|||||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
|
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
|
||||||
use Symfony\Component\Validator\Validation;
|
use Symfony\Component\Validator\Validation;
|
||||||
|
use Symfony\Component\Workflow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FrameworkExtension.
|
* FrameworkExtension.
|
||||||
@ -38,6 +39,7 @@ use Symfony\Component\Validator\Validation;
|
|||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
* @author Jeremy Mikola <jmikola@gmail.com>
|
* @author Jeremy Mikola <jmikola@gmail.com>
|
||||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class FrameworkExtension extends Extension
|
class FrameworkExtension extends Extension
|
||||||
{
|
{
|
||||||
@ -129,6 +131,7 @@ class FrameworkExtension extends Extension
|
|||||||
$this->registerTranslatorConfiguration($config['translator'], $container);
|
$this->registerTranslatorConfiguration($config['translator'], $container);
|
||||||
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
|
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
|
||||||
$this->registerCacheConfiguration($config['cache'], $container);
|
$this->registerCacheConfiguration($config['cache'], $container);
|
||||||
|
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
|
||||||
|
|
||||||
if ($this->isConfigEnabled($container, $config['router'])) {
|
if ($this->isConfigEnabled($container, $config['router'])) {
|
||||||
$this->registerRouterConfiguration($config['router'], $container, $loader);
|
$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.
|
* Loads the router configuration.
|
||||||
*
|
*
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
<xsd:element name="serializer" type="serializer" minOccurs="0" maxOccurs="1" />
|
<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="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
|
||||||
<xsd:element name="cache" type="cache" 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:all>
|
||||||
|
|
||||||
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
||||||
@ -224,4 +225,42 @@
|
|||||||
<xsd:attribute name="provider" type="xsd:string" />
|
<xsd:attribute name="provider" type="xsd:string" />
|
||||||
<xsd:attribute name="clearer" type="xsd:string" />
|
<xsd:attribute name="clearer" type="xsd:string" />
|
||||||
</xsd:complexType>
|
</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>
|
</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',
|
'directory' => '%kernel.cache_dir%/pools',
|
||||||
'default_redis_provider' => 'redis://localhost',
|
'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');
|
$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()
|
public function testRouter()
|
||||||
{
|
{
|
||||||
$container = $this->createContainerFromFile('full');
|
$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
|
||||||
|
=========
|
@ -11,29 +11,41 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Workflow;
|
namespace Symfony\Component\Workflow;
|
||||||
|
|
||||||
|
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Workflow\Exception\LogicException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class Definition
|
class Definition
|
||||||
{
|
{
|
||||||
private $class;
|
private $places = array();
|
||||||
private $states = array();
|
|
||||||
private $transitions = array();
|
private $transitions = array();
|
||||||
private $initialState;
|
|
||||||
|
|
||||||
public function __construct($class)
|
private $initialPlace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition constructor.
|
||||||
|
*
|
||||||
|
* @param string[] $places
|
||||||
|
* @param Transition[] $transitions
|
||||||
|
*/
|
||||||
|
public function __construct(array $places = array(), array $transitions = array())
|
||||||
{
|
{
|
||||||
$this->class = $class;
|
$this->addPlaces($places);
|
||||||
|
$this->addTransitions($transitions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getClass()
|
public function getInitialPlace()
|
||||||
{
|
{
|
||||||
return $this->class;
|
return $this->initialPlace;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStates()
|
public function getPlaces()
|
||||||
{
|
{
|
||||||
return $this->states;
|
return $this->places;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTransitions()
|
public function getTransitions()
|
||||||
@ -41,47 +53,58 @@ class Definition
|
|||||||
return $this->transitions;
|
return $this->transitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getInitialState()
|
public function setInitialPlace($place)
|
||||||
{
|
{
|
||||||
return $this->initialState;
|
if (!isset($this->places[$place])) {
|
||||||
|
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setInitialState($name)
|
$this->initialPlace = $place;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlace($place)
|
||||||
{
|
{
|
||||||
if (!isset($this->states[$name])) {
|
if (!preg_match('{^[\w\d_-]+$}', $place)) {
|
||||||
throw new \LogicException(sprintf('State "%s" cannot be the initial state as it does not exist.', $name));
|
throw new InvalidArgumentException(sprintf('The place "%s" contains invalid characters.', $name));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->initialState = $name;
|
if (!count($this->places)) {
|
||||||
|
$this->initialPlace = $place;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addState($name)
|
$this->places[$place] = $place;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPlaces(array $places)
|
||||||
{
|
{
|
||||||
if (!count($this->states)) {
|
foreach ($places as $place) {
|
||||||
$this->initialState = $name;
|
$this->addPlace($place);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->states[$name] = $name;
|
public function addTransitions(array $transitions)
|
||||||
|
{
|
||||||
|
foreach ($transitions as $transition) {
|
||||||
|
$this->addTransition($transition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addTransition(Transition $transition)
|
public function addTransition(Transition $transition)
|
||||||
{
|
{
|
||||||
if (isset($this->transitions[$transition->getName()])) {
|
$name = $transition->getName();
|
||||||
throw new \LogicException(sprintf('Transition "%s" is already defined.', $transition->getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($transition->getFroms() as $from) {
|
foreach ($transition->getFroms() as $from) {
|
||||||
if (!isset($this->states[$from])) {
|
if (!isset($this->places[$from])) {
|
||||||
throw new \LogicException(sprintf('State "%s" referenced in transition "%s" does not exist.', $from, $name));
|
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($transition->getTos() as $to) {
|
foreach ($transition->getTos() as $to) {
|
||||||
if (!isset($this->states[$to])) {
|
if (!isset($this->places[$to])) {
|
||||||
throw new \LogicException(sprintf('State "%s" referenced in transition "%s" does not exist.', $to, $name));
|
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->transitions[$transition->getName()] = $transition;
|
$this->transitions[$name] = $transition;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,13 @@
|
|||||||
namespace Symfony\Component\Workflow\Dumper;
|
namespace Symfony\Component\Workflow\Dumper;
|
||||||
|
|
||||||
use Symfony\Component\Workflow\Definition;
|
use Symfony\Component\Workflow\Definition;
|
||||||
|
use Symfony\Component\Workflow\Marking;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DumperInterface is the interface implemented by workflow dumper classes.
|
* DumperInterface is the interface implemented by workflow dumper classes.
|
||||||
*
|
*
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
interface DumperInterface
|
interface DumperInterface
|
||||||
{
|
{
|
||||||
@ -24,9 +26,10 @@ interface DumperInterface
|
|||||||
* Dumps a workflow definition.
|
* Dumps a workflow definition.
|
||||||
*
|
*
|
||||||
* @param Definition $definition A Definition instance
|
* @param Definition $definition A Definition instance
|
||||||
|
* @param Marking|null $marking A Marking instance
|
||||||
* @param array $options An array of options
|
* @param array $options An array of options
|
||||||
*
|
*
|
||||||
* @return string The representation of the workflow
|
* @return string The representation of the workflow
|
||||||
*/
|
*/
|
||||||
public function dump(Definition $definition, array $options = array());
|
public function dump(Definition $definition, Marking $marking = null, array $options = array());
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Component\Workflow\Dumper;
|
namespace Symfony\Component\Workflow\Dumper;
|
||||||
|
|
||||||
use Symfony\Component\Workflow\Definition;
|
use Symfony\Component\Workflow\Definition;
|
||||||
|
use Symfony\Component\Workflow\Marking;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GraphvizDumper dumps a workflow as a graphviz file.
|
* GraphvizDumper dumps a workflow as a graphviz file.
|
||||||
@ -21,72 +22,101 @@ use Symfony\Component\Workflow\Definition;
|
|||||||
* dot -Tpng workflow.dot > workflow.png
|
* dot -Tpng workflow.dot > workflow.png
|
||||||
*
|
*
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class GraphvizDumper implements DumperInterface
|
class GraphvizDumper implements DumperInterface
|
||||||
{
|
{
|
||||||
private $nodes;
|
private static $defaultOptions = array(
|
||||||
private $edges;
|
|
||||||
private $options = array(
|
|
||||||
'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'),
|
'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'),
|
||||||
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333', 'shape' => 'circle', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
|
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
|
||||||
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
|
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
* Dumps the workflow as a graphviz graph.
|
* Dumps the workflow as a graphviz graph.
|
||||||
*
|
*
|
||||||
* Available options:
|
* Available options:
|
||||||
*
|
*
|
||||||
* * graph: The default options for the whole graph
|
* * graph: The default options for the whole graph
|
||||||
* * node: The default options for nodes
|
* * node: The default options for nodes (places + transitions)
|
||||||
* * edge: The default options for edges
|
* * edge: The default options for edges
|
||||||
*
|
|
||||||
* @param Definition $definition A Definition instance
|
|
||||||
* @param array $options An array of options
|
|
||||||
*
|
|
||||||
* @return string The dot representation of the workflow
|
|
||||||
*/
|
*/
|
||||||
public function dump(Definition $definition, array $options = array())
|
public function dump(Definition $definition, Marking $marking = null, array $options = array())
|
||||||
{
|
{
|
||||||
foreach (array('graph', 'node', 'edge') as $key) {
|
$places = $this->findPlaces($definition, $marking);
|
||||||
if (isset($options[$key])) {
|
$transitions = $this->findTransitions($definition);
|
||||||
$this->options[$key] = array_merge($this->options[$key], $options[$key]);
|
$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();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->nodes = $this->findNodes($definition);
|
private function findPlaces(Definition $definition, Marking $marking = null)
|
||||||
$this->edges = $this->findEdges($definition);
|
|
||||||
|
|
||||||
return $this->startDot().$this->addNodes().$this->addEdges().$this->endDot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all nodes.
|
|
||||||
*
|
|
||||||
* @return array An array of all nodes
|
|
||||||
*/
|
|
||||||
private function findNodes(Definition $definition)
|
|
||||||
{
|
{
|
||||||
$nodes = array();
|
$places = array();
|
||||||
foreach ($definition->getStates() as $state) {
|
|
||||||
$nodes[$state] = array(
|
foreach ($definition->getPlaces() as $place) {
|
||||||
'attributes' => array_merge($this->options['node'], array('style' => $state == $definition->getInitialState() ? 'filled' : 'solid'))
|
$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 $nodes;
|
return $places;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function findTransitions(Definition $definition)
|
||||||
* Returns all nodes.
|
{
|
||||||
*
|
$transitions = array();
|
||||||
* @return string A string representation of all nodes
|
|
||||||
*/
|
foreach ($definition->getTransitions() as $name => $transition) {
|
||||||
private function addNodes()
|
$transitions[$name] = array(
|
||||||
|
'attributes' => array('shape' => 'box', 'regular' => true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addPlaces(array $places)
|
||||||
{
|
{
|
||||||
$code = '';
|
$code = '';
|
||||||
foreach ($this->nodes as $id => $node) {
|
|
||||||
$code .= sprintf(" node_%s [label=\"%s\", shape=%s%s];\n", $this->dotize($id), $id, $this->options['node']['shape'], $this->addAttributes($node['attributes']));
|
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;
|
return $code;
|
||||||
@ -94,72 +124,62 @@ class GraphvizDumper implements DumperInterface
|
|||||||
|
|
||||||
private function findEdges(Definition $definition)
|
private function findEdges(Definition $definition)
|
||||||
{
|
{
|
||||||
$edges = array();
|
$dotEdges = array();
|
||||||
|
|
||||||
foreach ($definition->getTransitions() as $transition) {
|
foreach ($definition->getTransitions() as $transition) {
|
||||||
foreach ($transition->getFroms() as $from) {
|
foreach ($transition->getFroms() as $from) {
|
||||||
|
$dotEdges[] = array(
|
||||||
|
'from' => $from,
|
||||||
|
'to' => $transition->getName(),
|
||||||
|
'direction' => 'from',
|
||||||
|
);
|
||||||
|
}
|
||||||
foreach ($transition->getTos() as $to) {
|
foreach ($transition->getTos() as $to) {
|
||||||
$edges[$from][] = array(
|
$dotEdges[] = array(
|
||||||
'name' => $transition->getName(),
|
'from' => $transition->getName(),
|
||||||
'to' => $to,
|
'to' => $to,
|
||||||
|
'direction' => 'to',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $dotEdges;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $edges;
|
private function addEdges($edges)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all edges.
|
|
||||||
*
|
|
||||||
* @return string A string representation of all edges
|
|
||||||
*/
|
|
||||||
private function addEdges()
|
|
||||||
{
|
{
|
||||||
$code = '';
|
$code = '';
|
||||||
foreach ($this->edges as $id => $edges) {
|
|
||||||
foreach ($edges as $edge) {
|
foreach ($edges as $edge) {
|
||||||
$code .= sprintf(" node_%s -> node_%s [label=\"%s\" style=\"%s\"];\n", $this->dotize($id), $this->dotize($edge['to']), $edge['name'], 'solid');
|
$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;
|
return $code;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private function startDot(array $options)
|
||||||
* Returns the start dot.
|
|
||||||
*
|
|
||||||
* @return string The string representation of a start dot
|
|
||||||
*/
|
|
||||||
private function startDot()
|
|
||||||
{
|
{
|
||||||
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
|
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
|
||||||
$this->addOptions($this->options['graph']),
|
$this->addOptions($options['graph']),
|
||||||
$this->addOptions($this->options['node']),
|
$this->addOptions($options['node']),
|
||||||
$this->addOptions($this->options['edge'])
|
$this->addOptions($options['edge'])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the end dot.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function endDot()
|
private function endDot()
|
||||||
{
|
{
|
||||||
return "}\n";
|
return "}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds attributes
|
|
||||||
*
|
|
||||||
* @param array $attributes An array of attributes
|
|
||||||
*
|
|
||||||
* @return string A comma separated list of attributes
|
|
||||||
*/
|
|
||||||
private function addAttributes($attributes)
|
private function addAttributes($attributes)
|
||||||
{
|
{
|
||||||
$code = array();
|
$code = array();
|
||||||
|
|
||||||
foreach ($attributes as $k => $v) {
|
foreach ($attributes as $k => $v) {
|
||||||
$code[] = sprintf('%s="%s"', $k, $v);
|
$code[] = sprintf('%s="%s"', $k, $v);
|
||||||
}
|
}
|
||||||
@ -167,16 +187,10 @@ class GraphvizDumper implements DumperInterface
|
|||||||
return $code ? ', '.implode(', ', $code) : '';
|
return $code ? ', '.implode(', ', $code) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds options
|
|
||||||
*
|
|
||||||
* @param array $options An array of options
|
|
||||||
*
|
|
||||||
* @return string A space separated list of options
|
|
||||||
*/
|
|
||||||
private function addOptions($options)
|
private function addOptions($options)
|
||||||
{
|
{
|
||||||
$code = array();
|
$code = array();
|
||||||
|
|
||||||
foreach ($options as $k => $v) {
|
foreach ($options as $k => $v) {
|
||||||
$code[] = sprintf('%s="%s"', $k, $v);
|
$code[] = sprintf('%s="%s"', $k, $v);
|
||||||
}
|
}
|
||||||
@ -184,13 +198,6 @@ class GraphvizDumper implements DumperInterface
|
|||||||
return implode(' ', $code);
|
return implode(' ', $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Dotizes an identifier.
|
|
||||||
*
|
|
||||||
* @param string $id The identifier to dotize
|
|
||||||
*
|
|
||||||
* @return string A dotized string
|
|
||||||
*/
|
|
||||||
private function dotize($id)
|
private function dotize($id)
|
||||||
{
|
{
|
||||||
return strtolower(preg_replace('/[^\w]/i', '_', $id));
|
return strtolower(preg_replace('/[^\w]/i', '_', $id));
|
||||||
|
@ -12,40 +12,47 @@
|
|||||||
namespace Symfony\Component\Workflow\Event;
|
namespace Symfony\Component\Workflow\Event;
|
||||||
|
|
||||||
use Symfony\Component\EventDispatcher\Event as BaseEvent;
|
use Symfony\Component\EventDispatcher\Event as BaseEvent;
|
||||||
|
use Symfony\Component\Workflow\Marking;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class Event extends BaseEvent
|
class Event extends BaseEvent
|
||||||
{
|
{
|
||||||
private $object;
|
private $subject;
|
||||||
private $state;
|
|
||||||
private $attributes;
|
|
||||||
|
|
||||||
public function __construct($object, $state, array $attributes = array())
|
private $marking;
|
||||||
|
|
||||||
|
private $transition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event constructor.
|
||||||
|
*
|
||||||
|
* @param mixed $subject
|
||||||
|
* @param Marking $marking
|
||||||
|
* @param Transition $transition
|
||||||
|
*/
|
||||||
|
public function __construct($subject, Marking $marking, Transition $transition)
|
||||||
{
|
{
|
||||||
$this->object = $object;
|
$this->subject = $subject;
|
||||||
$this->state = $state;
|
$this->marking = $marking;
|
||||||
$this->attributes = $attributes;
|
$this->transition = $transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getState()
|
public function getMarking()
|
||||||
{
|
{
|
||||||
return $this->state;
|
return $this->marking;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getObject()
|
public function getSubject()
|
||||||
{
|
{
|
||||||
return $this->object;
|
return $this->subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttribute($key)
|
public function getTransition()
|
||||||
{
|
{
|
||||||
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
|
return $this->transition;
|
||||||
}
|
|
||||||
|
|
||||||
public function hastAttribute($key)
|
|
||||||
{
|
|
||||||
return isset($this->attributes[$key]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,18 +13,19 @@ namespace Symfony\Component\Workflow\Event;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class GuardEvent extends Event
|
class GuardEvent extends Event
|
||||||
{
|
{
|
||||||
private $allowed = null;
|
private $blocked = false;
|
||||||
|
|
||||||
public function isAllowed()
|
public function isBlocked()
|
||||||
{
|
{
|
||||||
return $this->allowed;
|
return $this->blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAllowed($allowed)
|
public function setBlocked($blocked)
|
||||||
{
|
{
|
||||||
$this->allowed = (Boolean) $allowed;
|
$this->blocked = (bool) $blocked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,33 +11,46 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Workflow\EventListener;
|
namespace Symfony\Component\Workflow\EventListener;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Workflow\Event\Event;
|
use Symfony\Component\Workflow\Event\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
|
*/
|
||||||
class AuditTrailListener implements EventSubscriberInterface
|
class AuditTrailListener implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public function onEnter(Event $event)
|
private $logger;
|
||||||
|
|
||||||
|
public function __construct(LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
// FIXME: object "identity", timestamp, who, ...
|
$this->logger = $logger;
|
||||||
error_log('entering "'.$event->getState().'" generic for object of class '.get_class($event->getObject()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onLeave(Event $event)
|
public function onLeave(Event $event)
|
||||||
{
|
{
|
||||||
error_log('leaving "'.$event->getState().'" generic for object of class '.get_class($event->getObject()));
|
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)
|
public function onTransition(Event $event)
|
||||||
{
|
{
|
||||||
error_log('transition "'.$event->getState().'" generic for object of class '.get_class($event->getObject()));
|
$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()
|
public static function getSubscribedEvents()
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
// FIXME: add a way to listen to workflow.XXX.*
|
|
||||||
'workflow.transition' => array('onTransition'),
|
|
||||||
'workflow.leave' => array('onLeave'),
|
'workflow.leave' => array('onLeave'),
|
||||||
|
'workflow.transition' => array('onTransition'),
|
||||||
'workflow.enter' => array('onEnter'),
|
'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)
|
@ -11,33 +11,55 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Workflow;
|
namespace Symfony\Component\Workflow;
|
||||||
|
|
||||||
|
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class Registry
|
class Registry
|
||||||
{
|
{
|
||||||
private $workflows = array();
|
private $workflows = array();
|
||||||
|
|
||||||
public function __construct(array $workflows = array())
|
/**
|
||||||
|
* @param Workflow $workflow
|
||||||
|
* @param string $classname
|
||||||
|
*/
|
||||||
|
public function add(Workflow $workflow, $classname)
|
||||||
{
|
{
|
||||||
foreach ($workflows as $workflow) {
|
$this->workflows[] = array($workflow, $classname);
|
||||||
$this->add($workflow);
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function add(Workflow $workflow)
|
if (!$matched) {
|
||||||
{
|
throw new InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($subject)));
|
||||||
$this->workflows[] = $workflow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get($object)
|
return $matched;
|
||||||
{
|
|
||||||
foreach ($this->workflows as $workflow) {
|
|
||||||
if ($workflow->supports($object)) {
|
|
||||||
return $workflow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($object)));
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -11,17 +11,33 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Workflow;
|
namespace Symfony\Component\Workflow;
|
||||||
|
|
||||||
|
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class Transition
|
class Transition
|
||||||
{
|
{
|
||||||
private $name;
|
private $name;
|
||||||
private $froms = array();
|
|
||||||
private $tos = array();
|
|
||||||
|
|
||||||
|
private $froms;
|
||||||
|
|
||||||
|
private $tos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition constructor.
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @param string|string[] $froms
|
||||||
|
* @param string|string[] $tos
|
||||||
|
*/
|
||||||
public function __construct($name, $froms, $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->name = $name;
|
||||||
$this->froms = (array) $froms;
|
$this->froms = (array) $froms;
|
||||||
$this->tos = (array) $tos;
|
$this->tos = (array) $tos;
|
||||||
|
@ -14,195 +14,261 @@ namespace Symfony\Component\Workflow;
|
|||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
use Symfony\Component\Workflow\Event\Event;
|
use Symfony\Component\Workflow\Event\Event;
|
||||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||||
use Symfony\Component\Workflow\Event\TransitionEvent;
|
use Symfony\Component\Workflow\Exception\LogicException;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
|
||||||
use Symfony\Component\PropertyAccess\PropertyAccessor;
|
use Symfony\Component\Workflow\MarkingStore\UniqueTransitionOutputInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fabien Potencier <fabien@symfony.com>
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||||
*/
|
*/
|
||||||
class Workflow
|
class Workflow
|
||||||
{
|
{
|
||||||
private $name;
|
private $definition;
|
||||||
|
private $markingStore;
|
||||||
private $dispatcher;
|
private $dispatcher;
|
||||||
private $propertyAccessor;
|
private $name;
|
||||||
private $property = 'state';
|
|
||||||
private $stateTransitions = array();
|
|
||||||
private $states;
|
|
||||||
private $initialState;
|
|
||||||
private $class;
|
|
||||||
|
|
||||||
public function __construct($name, Definition $definition, EventDispatcherInterface $dispatcher = null)
|
public function __construct(Definition $definition, MarkingStoreInterface $markingStore, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->definition = $definition;
|
||||||
|
$this->markingStore = $markingStore;
|
||||||
$this->dispatcher = $dispatcher;
|
$this->dispatcher = $dispatcher;
|
||||||
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
|
$this->name = $name;
|
||||||
|
|
||||||
$this->states = $definition->getStates();
|
// If the marking can contain only one place, we should control the definition
|
||||||
$this->class = $definition->getClass();
|
if ($markingStore instanceof UniqueTransitionOutputInterface) {
|
||||||
$this->initialState = $definition->getInitialState();
|
foreach ($definition->getTransitions() as $transition) {
|
||||||
foreach ($definition->getTransitions() as $name => $transition) {
|
if (1 < count($transition->getTos())) {
|
||||||
$this->transitions[$name] = $transition;
|
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())));
|
||||||
foreach ($transition->getFroms() as $from) {
|
}
|
||||||
$this->stateTransitions[$from][$name] = $name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports($class)
|
/**
|
||||||
|
* Returns the object's Marking.
|
||||||
|
*
|
||||||
|
* @param object $subject A subject
|
||||||
|
*
|
||||||
|
* @return Marking The Marking
|
||||||
|
*
|
||||||
|
* @throws LogicException
|
||||||
|
*/
|
||||||
|
public function getMarking($subject)
|
||||||
{
|
{
|
||||||
return $class instanceof $this->class;
|
$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));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function can($object, $transition)
|
// 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)
|
||||||
{
|
{
|
||||||
if (!isset($this->transitions[$transition])) {
|
$transitions = $this->definition->getTransitions();
|
||||||
throw new \LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transition, $this->name));
|
|
||||||
|
if (!isset($transitions[$transitionName])) {
|
||||||
|
throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $this->dispatcher) {
|
$transition = $transitions[$transitionName];
|
||||||
$event = new GuardEvent($object, $this->getState($object));
|
|
||||||
|
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.guard.%s', $this->name, $transition), $event);
|
$marking = $this->getMarking($subject);
|
||||||
|
|
||||||
if (null !== $ret = $event->isAllowed()) {
|
return $this->doCan($subject, $marking, $transition);
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isset($this->stateTransitions[$this->getState($object)][$transition]);
|
/**
|
||||||
}
|
* Fire a transition.
|
||||||
|
*
|
||||||
public function getState($object)
|
* @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)
|
||||||
{
|
{
|
||||||
$state = $this->propertyAccessor->getValue($object, $this->property);
|
if (!$this->can($subject, $transitionName)) {
|
||||||
|
throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
|
||||||
// check if the object is already in the workflow
|
|
||||||
if (null === $state) {
|
|
||||||
$this->enter($object, $this->initialState, array());
|
|
||||||
|
|
||||||
$state = $this->propertyAccessor->getValue($object, $this->property);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that the object has a known state
|
// We can shortcut the getMarking method in order to boost performance,
|
||||||
if (!isset($this->states[$state])) {
|
// since the "can" method already checks the Marking state
|
||||||
throw new \LogicException(sprintf('State "%s" is not valid for workflow "%s".', $transition, $this->name));
|
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $state;
|
/**
|
||||||
}
|
* Returns all enabled transitions.
|
||||||
|
*
|
||||||
public function apply($object, $transition, array $attributes = array())
|
* @param object $subject A subject
|
||||||
|
*
|
||||||
|
* @return Transition[] All enabled transitions
|
||||||
|
*/
|
||||||
|
public function getEnabledTransitions($subject)
|
||||||
{
|
{
|
||||||
$current = $this->getState($object);
|
$enabled = array();
|
||||||
|
|
||||||
if (!$this->can($object, $transition)) {
|
$marking = $this->getMarking($subject);
|
||||||
throw new \LogicException(sprintf('Unable to apply transition "%s" from state "%s" for workflow "%s".', $transition, $current, $this->name));
|
|
||||||
|
foreach ($this->definition->getTransitions() as $transition) {
|
||||||
|
if ($this->doCan($subject, $marking, $transition)) {
|
||||||
|
$enabled[$transition->getName()] = $transition;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$transition = $this->determineTransition($current, $transition);
|
return $enabled;
|
||||||
|
|
||||||
$this->leave($object, $current, $attributes);
|
|
||||||
|
|
||||||
$state = $this->transition($object, $current, $transition, $attributes);
|
|
||||||
|
|
||||||
$this->enter($object, $state, $attributes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAvailableTransitions($object)
|
public function getName()
|
||||||
{
|
{
|
||||||
return array_keys($this->stateTransitions[$this->getState($object)]);
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getNextStates($object)
|
private function doCan($subject, Marking $marking, Transition $transition)
|
||||||
{
|
{
|
||||||
if (!$stateTransitions = $this->stateTransitions[$this->getState($object)]) {
|
foreach ($transition->getFroms() as $place) {
|
||||||
return array();
|
if (!$marking->has($place)) {
|
||||||
}
|
return false;
|
||||||
|
|
||||||
$states = array();
|
|
||||||
foreach ($stateTransitions as $transition) {
|
|
||||||
foreach ($this->transitions[$transition]->getTos() as $to) {
|
|
||||||
$states[] = $to;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $states;
|
if (true === $this->guardTransition($subject, $marking, $transition)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setStateProperty($property)
|
return true;
|
||||||
{
|
|
||||||
$this->property = $property;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPropertyAccessor(PropertyAccessor $propertyAccessor)
|
private function guardTransition($subject, Marking $marking, Transition $transition)
|
||||||
{
|
|
||||||
$this->propertyAccessor = $propertyAccessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __call($method, $arguments)
|
|
||||||
{
|
|
||||||
if (!count($arguments)) {
|
|
||||||
throw new BadMethodCallException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->apply($arguments[0], $method, array_slice($arguments, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function leave($object, $state, $attributes)
|
|
||||||
{
|
{
|
||||||
if (null === $this->dispatcher) {
|
if (null === $this->dispatcher) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.leave', $this->name), new Event($object, $state, $attributes));
|
$event = new GuardEvent($subject, $marking, $transition);
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), new Event($object, $state, $attributes));
|
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $state), new Event($object, $state, $attributes));
|
$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 transition($object, $current, Transition $transition, $attributes)
|
private function leave($subject, Transition $transition, Marking $marking)
|
||||||
{
|
{
|
||||||
$state = null;
|
if (null !== $this->dispatcher) {
|
||||||
$tos = $transition->getTos();
|
$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) {
|
if (null !== $this->dispatcher) {
|
||||||
// the generic event cannot change the next state
|
$this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $place), $event);
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.transition', $this->name), new Event($object, $current, $attributes));
|
}
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.transition', $this->name), new Event($object, $current, $attributes));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$event = new TransitionEvent($object, $current, $attributes);
|
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);
|
$this->dispatcher->dispatch(sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()), $event);
|
||||||
$state = $event->getNextState();
|
|
||||||
|
|
||||||
if (null !== $state && !in_array($state, $tos)) {
|
|
||||||
throw new \LogicException(sprintf('Transition "%s" cannot go to state "%s" for workflow "%s"', $transition->getName(), $state, $this->name));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $state) {
|
private function enter($subject, Transition $transition, Marking $marking)
|
||||||
if (count($tos) > 1) {
|
|
||||||
throw new \LogicException(sprintf('Unable to apply transition "%s" as the new state is not unique for workflow "%s".', $transition->getName(), $this->name));
|
|
||||||
}
|
|
||||||
|
|
||||||
$state = $tos[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $state;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function enter($object, $state, $attributes)
|
|
||||||
{
|
{
|
||||||
$this->propertyAccessor->setValue($object, $this->property, $state);
|
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) {
|
if (null !== $this->dispatcher) {
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.enter', $this->name), new Event($object, $state, $attributes));
|
$this->dispatcher->dispatch(sprintf('workflow.%s.enter.%s', $this->name, $place), $event);
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.enter', $this->name), new Event($object, $state, $attributes));
|
}
|
||||||
$this->dispatcher->dispatch(sprintf('workflow.%s.enter.%s', $this->name, $state), new Event($object, $state, $attributes));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function determineTransition($current, $transition)
|
private function announce($subject, Transition $initialTransition, Marking $marking)
|
||||||
{
|
{
|
||||||
return $this->transitions[$transition];
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "symfony/workflow",
|
"name": "symfony/workflow",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"description": "Symfony Workflow Component",
|
"description": "Symfony Workflow Component",
|
||||||
"keywords": [],
|
"keywords": ["workflow", "petrinet", "place", "transition"],
|
||||||
"homepage": "http://symfony.com",
|
"homepage": "http://symfony.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"authors": [
|
"authors": [
|
||||||
@ -10,27 +10,31 @@
|
|||||||
"name": "Fabien Potencier",
|
"name": "Fabien Potencier",
|
||||||
"email": "fabien@symfony.com"
|
"email": "fabien@symfony.com"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Grégoire Pineau",
|
||||||
|
"email": "lyrixx@lyrixx.info"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Symfony Community",
|
"name": "Symfony Community",
|
||||||
"homepage": "http://symfony.com/contributors"
|
"homepage": "http://symfony.com/contributors"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.3",
|
"php": ">=5.5.9"
|
||||||
"symfony/event-dispatcher": "~2.1",
|
|
||||||
"symfony/property-access": "~2.3"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"psr/log": "~1.0",
|
||||||
|
"symfony/event-dispatcher": "~2.1|~3.0",
|
||||||
|
"symfony/property-access": "~2.3|~3.0",
|
||||||
"twig/twig": "~1.14"
|
"twig/twig": "~1.14"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-0": { "Symfony\\Component\\Workflow\\": "" }
|
"psr-4": { "Symfony\\Component\\Workflow\\": "" }
|
||||||
},
|
},
|
||||||
"target-dir": "Symfony/Component/Workflow",
|
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "2.5-dev"
|
"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