[Workflow] Added initial set of files

This commit is contained in:
Grégoire Pineau 2016-03-25 16:43:30 +01:00 committed by Fabien Potencier
parent 17d59a7c66
commit 078e27f139
43 changed files with 2040 additions and 288 deletions

View File

@ -72,6 +72,7 @@
"symfony/validator": "self.version",
"symfony/var-dumper": "self.version",
"symfony/web-profiler-bundle": "self.version",
"symfony/workflow": "self.version",
"symfony/yaml": "self.version"
},
"require-dev": {

View 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';
}
}

View File

@ -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);
}
}

View File

@ -103,6 +103,7 @@ class Configuration implements ConfigurationInterface
$this->addSsiSection($rootNode);
$this->addFragmentsSection($rootNode);
$this->addProfilerSection($rootNode);
$this->addWorkflowSection($rootNode);
$this->addRouterSection($rootNode);
$this->addSessionSection($rootNode);
$this->addRequestSection($rootNode);
@ -226,6 +227,99 @@ class Configuration implements ConfigurationInterface
;
}
private function addWorkflowSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('workflows')
->useAttributeAsKey('name')
->prototype('array')
->children()
->arrayNode('marking_store')
->isRequired()
->children()
->enumNode('type')
->values(array('property_accessor', 'scalar'))
->end()
->arrayNode('arguments')
->beforeNormalization()
->ifString()
->then(function ($v) { return array($v); })
->end()
->prototype('scalar')
->end()
->end()
->scalarNode('service')
->cannotBeEmpty()
->end()
->end()
->validate()
->always(function ($v) {
if (isset($v['type']) && isset($v['service'])) {
throw new \InvalidArgumentException('"type" and "service" could not be used together.');
}
return $v;
})
->end()
->end()
->arrayNode('supports')
->isRequired()
->beforeNormalization()
->ifString()
->then(function ($v) { return array($v); })
->end()
->prototype('scalar')
->cannotBeEmpty()
->validate()
->ifTrue(function ($v) { return !class_exists($v); })
->thenInvalid('The supported class %s does not exist.')
->end()
->end()
->end()
->arrayNode('places')
->isRequired()
->requiresAtLeastOneElement()
->prototype('scalar')
->cannotBeEmpty()
->end()
->end()
->arrayNode('transitions')
->useAttributeAsKey('name')
->isRequired()
->requiresAtLeastOneElement()
->prototype('array')
->children()
->arrayNode('from')
->beforeNormalization()
->ifString()
->then(function ($v) { return array($v); })
->end()
->requiresAtLeastOneElement()
->prototype('scalar')
->cannotBeEmpty()
->end()
->end()
->arrayNode('to')
->beforeNormalization()
->ifString()
->then(function ($v) { return array($v); })
->end()
->requiresAtLeastOneElement()
->prototype('scalar')
->cannotBeEmpty()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()
;
}
private function addRouterSection(ArrayNodeDefinition $rootNode)
{
$rootNode

View File

@ -31,6 +31,7 @@ use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Workflow;
/**
* FrameworkExtension.
@ -38,6 +39,7 @@ use Symfony\Component\Validator\Validation;
* @author Fabien Potencier <fabien@symfony.com>
* @author Jeremy Mikola <jmikola@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class FrameworkExtension extends Extension
{
@ -129,6 +131,7 @@ class FrameworkExtension extends Extension
$this->registerTranslatorConfiguration($config['translator'], $container);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerCacheConfiguration($config['cache'], $container);
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
if ($this->isConfigEnabled($container, $config['router'])) {
$this->registerRouterConfiguration($config['router'], $container, $loader);
@ -346,6 +349,54 @@ class FrameworkExtension extends Extension
}
}
/**
* Loads the workflow configuration.
*
* @param array $workflows A workflow configuration array
* @param ContainerBuilder $container A ContainerBuilder instance
* @param XmlFileLoader $loader An XmlFileLoader instance
*/
private function registerWorkflowConfiguration(array $workflows, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!$workflows) {
return;
}
$loader->load('workflow.xml');
$registryDefinition = $container->getDefinition('workflow.registry');
foreach ($workflows as $name => $workflow) {
$definitionDefinition = new Definition(Workflow\Definition::class);
$definitionDefinition->addMethodCall('addPlaces', array($workflow['places']));
foreach ($workflow['transitions'] as $transitionName => $transition) {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
}
if (isset($workflow['marking_store']['type'])) {
$markingStoreDefinition = new DefinitionDecorator('workflow.marking_store.'.$workflow['marking_store']['type']);
foreach ($workflow['marking_store']['arguments'] as $argument) {
$markingStoreDefinition->addArgument($argument);
}
} else {
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
}
$workflowDefinition = new DefinitionDecorator('workflow.abstract');
$workflowDefinition->replaceArgument(0, $definitionDefinition);
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
$workflowDefinition->replaceArgument(3, $name);
$workflowId = 'workflow.'.$name;
$container->setDefinition($workflowId, $workflowDefinition);
foreach ($workflow['supports'] as $supportedClass) {
$registryDefinition->addMethodCall('add', array(new Reference($workflowId), $supportedClass));
}
}
}
/**
* Loads the router configuration.
*

View File

@ -26,6 +26,7 @@
<xsd:element name="serializer" type="serializer" minOccurs="0" maxOccurs="1" />
<xsd:element name="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="workflows" type="workflows" minOccurs="0" maxOccurs="1" />
</xsd:all>
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@ -224,4 +225,42 @@
<xsd:attribute name="provider" type="xsd:string" />
<xsd:attribute name="clearer" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="workflows">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="workflow" type="workflow" />
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="workflow">
<xsd:sequence>
<xsd:element name="marking-store" type="marking_store" />
<xsd:element name="supports" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="places" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="transitions" type="transitions" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="marking_store">
<xsd:sequence>
<xsd:element name="type" type="xsd:string" minOccurs="0" maxOccurs="1" />
<xsd:element name="arguments" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="service" type="xsd:string" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="transitions">
<xsd:sequence>
<xsd:element name="transition" type="transition" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="transition">
<xsd:sequence>
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:schema>

View File

@ -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>

View File

@ -273,6 +273,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
'directory' => '%kernel.cache_dir%/pools',
'default_redis_provider' => 'redis://localhost',
),
'workflows' => array(),
);
}
}

View File

@ -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',
),
),
),
),
),
));

View File

@ -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>

View File

@ -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

View File

@ -117,6 +117,13 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertFalse($container->hasDefinition('data_collector.config'), '->registerProfilerConfiguration() does not load collectors.xml');
}
public function testWorkflow()
{
$container = $this->createContainerFromFile('workflow');
$this->assertTrue($container->hasDefinition('workflow.my_workflow'));
}
public function testRouter()
{
$container = $this->createContainerFromFile('full');

View File

@ -0,0 +1,2 @@
CHANGELOG
=========

View File

@ -11,29 +11,41 @@
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
use Symfony\Component\Workflow\Exception\LogicException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Definition
{
private $class;
private $states = array();
private $places = 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()
@ -41,47 +53,58 @@ class Definition
return $this->transitions;
}
public function getInitialState()
public function setInitialPlace($place)
{
return $this->initialState;
}
public function setInitialState($name)
{
if (!isset($this->states[$name])) {
throw new \LogicException(sprintf('State "%s" cannot be the initial state as it does not exist.', $name));
if (!isset($this->places[$place])) {
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
}
$this->initialState = $name;
$this->initialPlace = $place;
}
public function addState($name)
public function addPlace($place)
{
if (!count($this->states)) {
$this->initialState = $name;
if (!preg_match('{^[\w\d_-]+$}', $place)) {
throw new InvalidArgumentException(sprintf('The place "%s" contains invalid characters.', $name));
}
$this->states[$name] = $name;
if (!count($this->places)) {
$this->initialPlace = $place;
}
$this->places[$place] = $place;
}
public function addPlaces(array $places)
{
foreach ($places as $place) {
$this->addPlace($place);
}
}
public function addTransitions(array $transitions)
{
foreach ($transitions as $transition) {
$this->addTransition($transition);
}
}
public function addTransition(Transition $transition)
{
if (isset($this->transitions[$transition->getName()])) {
throw new \LogicException(sprintf('Transition "%s" is already defined.', $transition->getName()));
}
$name = $transition->getName();
foreach ($transition->getFroms() as $from) {
if (!isset($this->states[$from])) {
throw new \LogicException(sprintf('State "%s" referenced in transition "%s" does not exist.', $from, $name));
if (!isset($this->places[$from])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name));
}
}
foreach ($transition->getTos() as $to) {
if (!isset($this->states[$to])) {
throw new \LogicException(sprintf('State "%s" referenced in transition "%s" does not exist.', $to, $name));
if (!isset($this->places[$to])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name));
}
}
$this->transitions[$transition->getName()] = $transition;
$this->transitions[$name] = $transition;
}
}

View File

@ -12,21 +12,24 @@
namespace Symfony\Component\Workflow\Dumper;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
/**
* DumperInterface is the interface implemented by workflow dumper classes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface DumperInterface
{
/**
* Dumps a workflow definition.
*
* @param Definition $definition A Definition instance
* @param array $options An array of options
* @param Definition $definition A Definition instance
* @param Marking|null $marking A Marking instance
* @param array $options An array of options
*
* @return string The representation of the workflow
*/
public function dump(Definition $definition, array $options = array());
public function dump(Definition $definition, Marking $marking = null, array $options = array());
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Workflow\Dumper;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
/**
* GraphvizDumper dumps a workflow as a graphviz file.
@ -21,72 +22,101 @@ use Symfony\Component\Workflow\Definition;
* dot -Tpng workflow.dot > workflow.png
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class GraphvizDumper implements DumperInterface
{
private $nodes;
private $edges;
private $options = array(
private static $defaultOptions = array(
'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'),
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333', 'shape' => 'circle', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
);
/**
* {@inheritdoc}
*
* Dumps the workflow as a graphviz graph.
*
* Available options:
*
* * graph: The default options for the whole graph
* * node: The default options for nodes
* * node: The default options for nodes (places + transitions)
* * 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) {
if (isset($options[$key])) {
$this->options[$key] = array_merge($this->options[$key], $options[$key]);
}
}
$places = $this->findPlaces($definition, $marking);
$transitions = $this->findTransitions($definition);
$edges = $this->findEdges($definition);
$this->nodes = $this->findNodes($definition);
$this->edges = $this->findEdges($definition);
$options = array_replace_recursive(self::$defaultOptions, $options);
return $this->startDot().$this->addNodes().$this->addEdges().$this->endDot();
return $this->startDot($options)
.$this->addPlaces($places)
.$this->addTransitions($transitions)
.$this->addEdges($edges)
.$this->endDot();
}
/**
* Finds all nodes.
*
* @return array An array of all nodes
*/
private function findNodes(Definition $definition)
private function findPlaces(Definition $definition, Marking $marking = null)
{
$nodes = array();
foreach ($definition->getStates() as $state) {
$nodes[$state] = array(
'attributes' => array_merge($this->options['node'], array('style' => $state == $definition->getInitialState() ? 'filled' : 'solid'))
$places = array();
foreach ($definition->getPlaces() as $place) {
$attributes = array();
if ($place === $definition->getInitialPlace()) {
$attributes['style'] = 'filled';
}
if ($marking && $marking->has($place)) {
$attributes['color'] = '#FF0000';
$attributes['shape'] = 'doublecircle';
}
$places[$place] = array(
'attributes' => $attributes,
);
}
return $nodes;
return $places;
}
/**
* Returns all nodes.
*
* @return string A string representation of all nodes
*/
private function addNodes()
private function findTransitions(Definition $definition)
{
$transitions = array();
foreach ($definition->getTransitions() as $name => $transition) {
$transitions[$name] = array(
'attributes' => array('shape' => 'box', 'regular' => true),
);
}
return $transitions;
}
private function addPlaces(array $places)
{
$code = '';
foreach ($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;
@ -94,72 +124,62 @@ class GraphvizDumper implements DumperInterface
private function findEdges(Definition $definition)
{
$edges = array();
$dotEdges = array();
foreach ($definition->getTransitions() as $transition) {
foreach ($transition->getFroms() as $from) {
foreach ($transition->getTos() as $to) {
$edges[$from][] = array(
'name' => $transition->getName(),
'to' => $to,
);
}
$dotEdges[] = array(
'from' => $from,
'to' => $transition->getName(),
'direction' => 'from',
);
}
foreach ($transition->getTos() as $to) {
$dotEdges[] = array(
'from' => $transition->getName(),
'to' => $to,
'direction' => 'to',
);
}
}
return $edges;
return $dotEdges;
}
/**
* Returns all edges.
*
* @return string A string representation of all edges
*/
private function addEdges()
private function addEdges($edges)
{
$code = '';
foreach ($this->edges as $id => $edges) {
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');
}
foreach ($edges as $edge) {
$code .= sprintf(" %s_%s -> %s_%s [style=\"solid\"];\n",
'from' === $edge['direction'] ? 'place' : 'transition',
$this->dotize($edge['from']),
'from' === $edge['direction'] ? 'transition' : 'place',
$this->dotize($edge['to'])
);
}
return $code;
}
/**
* Returns the start dot.
*
* @return string The string representation of a start dot
*/
private function startDot()
private function startDot(array $options)
{
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
$this->addOptions($this->options['graph']),
$this->addOptions($this->options['node']),
$this->addOptions($this->options['edge'])
$this->addOptions($options['graph']),
$this->addOptions($options['node']),
$this->addOptions($options['edge'])
);
}
/**
* Returns the end dot.
*
* @return string
*/
private function endDot()
{
return "}\n";
}
/**
* Adds attributes
*
* @param array $attributes An array of attributes
*
* @return string A comma separated list of attributes
*/
private function addAttributes($attributes)
{
$code = array();
foreach ($attributes as $k => $v) {
$code[] = sprintf('%s="%s"', $k, $v);
}
@ -167,16 +187,10 @@ class GraphvizDumper implements DumperInterface
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)
{
$code = array();
foreach ($options as $k => $v) {
$code[] = sprintf('%s="%s"', $k, $v);
}
@ -184,13 +198,6 @@ class GraphvizDumper implements DumperInterface
return implode(' ', $code);
}
/**
* Dotizes an identifier.
*
* @param string $id The identifier to dotize
*
* @return string A dotized string
*/
private function dotize($id)
{
return strtolower(preg_replace('/[^\w]/i', '_', $id));

View File

@ -12,40 +12,47 @@
namespace Symfony\Component\Workflow\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Transition;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Event extends BaseEvent
{
private $object;
private $state;
private $attributes;
private $subject;
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->state = $state;
$this->attributes = $attributes;
$this->subject = $subject;
$this->marking = $marking;
$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;
}
public function hastAttribute($key)
{
return isset($this->attributes[$key]);
return $this->transition;
}
}

View File

@ -13,18 +13,19 @@ namespace Symfony\Component\Workflow\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
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;
}
}

View File

@ -11,33 +11,46 @@
namespace Symfony\Component\Workflow\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class AuditTrailListener implements EventSubscriberInterface
{
public function onEnter(Event $event)
private $logger;
public function __construct(LoggerInterface $logger)
{
// FIXME: object "identity", timestamp, who, ...
error_log('entering "'.$event->getState().'" generic for object of class '.get_class($event->getObject()));
$this->logger = $logger;
}
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)
{
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()
{
return array(
// FIXME: add a way to listen to workflow.XXX.*
'workflow.transition' => array('onTransition'),
'workflow.leave' => array('onLeave'),
'workflow.transition' => array('onTransition'),
'workflow.enter' => array('onEnter'),
);
}

View 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>
*/
interface ExceptionInterface
{
}

View 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 InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View 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
{
}

View 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.

View 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;
}
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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()));
}
}

View File

@ -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
{
}

View 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)

View File

@ -11,33 +11,55 @@
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Registry
{
private $workflows = array();
public function __construct(array $workflows = array())
/**
* @param Workflow $workflow
* @param string $classname
*/
public function add(Workflow $workflow, $classname)
{
foreach ($workflows as $workflow) {
$this->add($workflow);
}
$this->workflows[] = array($workflow, $classname);
}
public function add(Workflow $workflow)
public function get($subject, $workflowName = null)
{
$this->workflows[] = $workflow;
}
$matched = null;
public function get($object)
{
foreach ($this->workflows as $workflow) {
if ($workflow->supports($object)) {
return $workflow;
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;
}
}
throw new \InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($object)));
if (!$matched) {
throw new InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($subject)));
}
return $matched;
}
private function supports(Workflow $workflow, $classname, $subject, $name)
{
if (!$subject instanceof $classname) {
return false;
}
if (null === $name) {
return true;
}
return $name === $workflow->getName();
}
}

View 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')));
}
}

View File

@ -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"];
}
';
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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());
}
}

View 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
{
}

View 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());
}
}

View 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)
{
}
}

View File

@ -11,17 +11,33 @@
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Transition
{
private $name;
private $froms = 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)
{
if (!preg_match('{^[\w\d_-]+$}', $name)) {
throw new InvalidArgumentException(sprintf('The transition "%s" contains invalid characters.', $name));
}
$this->name = $name;
$this->froms = (array) $froms;
$this->tos = (array) $tos;

View File

@ -14,195 +14,261 @@ namespace Symfony\Component\Workflow;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Event\TransitionEvent;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\UniqueTransitionOutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Workflow
{
private $name;
private $definition;
private $markingStore;
private $dispatcher;
private $propertyAccessor;
private $property = 'state';
private $stateTransitions = array();
private $states;
private $initialState;
private $class;
private $name;
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->propertyAccessor = PropertyAccess::createPropertyAccessor();
$this->name = $name;
$this->states = $definition->getStates();
$this->class = $definition->getClass();
$this->initialState = $definition->getInitialState();
foreach ($definition->getTransitions() as $name => $transition) {
$this->transitions[$name] = $transition;
foreach ($transition->getFroms() as $from) {
$this->stateTransitions[$from][$name] = $name;
// If the marking can contain only one place, we should control the definition
if ($markingStore instanceof UniqueTransitionOutputInterface) {
foreach ($definition->getTransitions() as $transition) {
if (1 < count($transition->getTos())) {
throw new LogicException(sprintf('The marking store (%s) of workflow "%s" can not store many places. But the transition "%s" has too many output (%d). Only one is accepted.', get_class($markingStore), $this->name, $transition->getName(), count($transition->getTos())));
}
}
}
}
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);
public function can($object, $transition)
{
if (!isset($this->transitions[$transition])) {
throw new \LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transition, $this->name));
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));
}
if (null !== $this->dispatcher) {
$event = new GuardEvent($object, $this->getState($object));
// 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());
}
$this->dispatcher->dispatch(sprintf('workflow.%s.guard.%s', $this->name, $transition), $event);
// 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.';
}
if (null !== $ret = $event->isAllowed()) {
return $ret;
throw new LogicException($message);
}
}
return isset($this->stateTransitions[$this->getState($object)][$transition]);
// Because the marking could have been initialized, we update the subject
$this->markingStore->setMarking($subject, $marking);
return $marking;
}
public function getState($object)
/**
* 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)
{
$state = $this->propertyAccessor->getValue($object, $this->property);
$transitions = $this->definition->getTransitions();
// 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);
if (!isset($transitions[$transitionName])) {
throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name));
}
// check that the object has a known state
if (!isset($this->states[$state])) {
throw new \LogicException(sprintf('State "%s" is not valid for workflow "%s".', $transition, $this->name));
}
$transition = $transitions[$transitionName];
return $state;
$marking = $this->getMarking($subject);
return $this->doCan($subject, $marking, $transition);
}
public function apply($object, $transition, array $attributes = array())
/**
* Fire a transition.
*
* @param object $subject A subject
* @param string $transitionName A transition
*
* @return Marking The new Marking
*
* @throws LogicException If the transition is not applicable
* @throws LogicException If the transition does not exist
*/
public function apply($subject, $transitionName)
{
$current = $this->getState($object);
if (!$this->can($object, $transition)) {
throw new \LogicException(sprintf('Unable to apply transition "%s" from state "%s" for workflow "%s".', $transition, $current, $this->name));
if (!$this->can($subject, $transitionName)) {
throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
}
$transition = $this->determineTransition($current, $transition);
// We can shortcut the getMarking method in order to boost performance,
// since the "can" method already checks the Marking state
$marking = $this->markingStore->getMarking($subject);
$this->leave($object, $current, $attributes);
$transition = $this->definition->getTransitions()[$transitionName];
$state = $this->transition($object, $current, $transition, $attributes);
$this->leave($subject, $transition, $marking);
$this->enter($object, $state, $attributes);
$this->transition($subject, $transition, $marking);
$this->enter($subject, $transition, $marking);
$this->announce($subject, $transition, $marking);
$this->markingStore->setMarking($subject, $marking);
return $marking;
}
public function getAvailableTransitions($object)
/**
* Returns all enabled transitions.
*
* @param object $subject A subject
*
* @return Transition[] All enabled transitions
*/
public function getEnabledTransitions($subject)
{
return array_keys($this->stateTransitions[$this->getState($object)]);
}
$enabled = array();
public function getNextStates($object)
{
if (!$stateTransitions = $this->stateTransitions[$this->getState($object)]) {
return array();
}
$marking = $this->getMarking($subject);
$states = array();
foreach ($stateTransitions as $transition) {
foreach ($this->transitions[$transition]->getTos() as $to) {
$states[] = $to;
foreach ($this->definition->getTransitions() as $transition) {
if ($this->doCan($subject, $marking, $transition)) {
$enabled[$transition->getName()] = $transition;
}
}
return $states;
return $enabled;
}
public function setStateProperty($property)
public function getName()
{
$this->property = $property;
return $this->name;
}
public function setPropertyAccessor(PropertyAccessor $propertyAccessor)
private function doCan($subject, Marking $marking, Transition $transition)
{
$this->propertyAccessor = $propertyAccessor;
}
public function __call($method, $arguments)
{
if (!count($arguments)) {
throw new BadMethodCallException();
foreach ($transition->getFroms() as $place) {
if (!$marking->has($place)) {
return false;
}
}
return $this->apply($arguments[0], $method, array_slice($arguments, 1));
if (true === $this->guardTransition($subject, $marking, $transition)) {
return false;
}
return true;
}
private function leave($object, $state, $attributes)
private function guardTransition($subject, Marking $marking, Transition $transition)
{
if (null === $this->dispatcher) {
return;
}
$this->dispatcher->dispatch(sprintf('workflow.leave', $this->name), new Event($object, $state, $attributes));
$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));
$event = new GuardEvent($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.guard', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.guard', $this->name), $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.guard.%s', $this->name, $transition->getName()), $event);
return $event->isBlocked();
}
private function transition($object, $current, Transition $transition, $attributes)
private function leave($subject, Transition $transition, Marking $marking)
{
$state = null;
$tos = $transition->getTos();
if (null !== $this->dispatcher) {
// the generic event cannot change the next state
$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 Event($subject, $marking, $transition);
$event = new TransitionEvent($object, $current, $attributes);
$this->dispatcher->dispatch(sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()), $event);
$state = $event->getNextState();
$this->dispatcher->dispatch('workflow.leave', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), $event);
}
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));
foreach ($transition->getFroms() as $place) {
$marking->unmark($place);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $place), $event);
}
}
if (null === $state) {
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)
private function transition($subject, Transition $transition, Marking $marking)
{
$this->propertyAccessor->setValue($object, $this->property, $state);
if (null === $this->dispatcher) {
return;
}
$event = new Event($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.transition', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.transition', $this->name), $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()), $event);
}
private function enter($subject, Transition $transition, Marking $marking)
{
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(sprintf('workflow.enter', $this->name), new Event($object, $state, $attributes));
$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));
$event = new Event($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.enter', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.enter', $this->name), $event);
}
foreach ($transition->getTos() as $place) {
$marking->mark($place);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(sprintf('workflow.%s.enter.%s', $this->name, $place), $event);
}
}
}
private function 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);
}
}
}
}

View File

@ -2,7 +2,7 @@
"name": "symfony/workflow",
"type": "library",
"description": "Symfony Workflow Component",
"keywords": [],
"keywords": ["workflow", "petrinet", "place", "transition"],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
@ -10,27 +10,31 @@
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Grégoire Pineau",
"email": "lyrixx@lyrixx.info"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3",
"symfony/event-dispatcher": "~2.1",
"symfony/property-access": "~2.3"
"php": ">=5.5.9"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/event-dispatcher": "~2.1|~3.0",
"symfony/property-access": "~2.3|~3.0",
"twig/twig": "~1.14"
},
"autoload": {
"psr-0": { "Symfony\\Component\\Workflow\\": "" }
"psr-4": { "Symfony\\Component\\Workflow\\": "" }
},
"target-dir": "Symfony/Component/Workflow",
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.5-dev"
"dev-master": "3.2-dev"
}
}
}

View 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>