feature #11882 [Workflow] Introducing the workflow component (fabpot, lyrixx)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Workflow] Introducing the workflow component

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | not yet
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

TODO:

* [x] Add tests
* [x] Add PHP doc
* [x] Add Symfony fullstack integration (Config, DIC, command to dump the state-machine into graphiz format)

So why another component?

This component take another approach that what you can find on [Packagist](https://packagist.org/search/?q=state%20machine).

Here, the workflow component is not tied to a specific object like with [Finite](https://github.com/yohang/Finite). It means that the component workflow is stateless and can be a symfony service.

Some code:

```php
#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;

class Foo
{
    public $marking;

    public function __construct($init = 'a')
    {
        $this->marking = $init;
        $this->marking = [$init => true];
    }
}

$fooDefinition = new Definition(Foo::class);
$fooDefinition->addPlaces([
    'a', 'b', 'c', 'd', 'e', 'f', 'g',
]);

//                                           name  from        to
$fooDefinition->addTransition(new Transition('t1', 'a',        ['b', 'c']));
$fooDefinition->addTransition(new Transition('t2', ['b', 'c'],  'd'));
$fooDefinition->addTransition(new Transition('t3', 'd',         'e'));
$fooDefinition->addTransition(new Transition('t4', 'd',         'f'));
$fooDefinition->addTransition(new Transition('t5', 'e',         'g'));
$fooDefinition->addTransition(new Transition('t6', 'f',         'g'));

$graph = (new GraphvizDumper())->dump($fooDefinition);

$ed = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch(), new \Monolog\Logger('app'));

// $workflow = new Workflow($fooDefinition, new ScalarMarkingStore(), $ed);
$workflow = new Workflow($fooDefinition, new PropertyAccessorMarkingStore(), $ed);

$foo = new Foo(isset($argv[1]) ? $argv[1] : 'a');

$graph = (new GraphvizDumper())->dump($fooDefinition, $workflow->getMarking($foo));

dump([
    'AvailableTransitions' => $workflow->getAvailableTransitions($foo),
    'CurrentMarking' => clone $workflow->getMarking($foo),
    'can validate t1' => $workflow->can($foo, 't1'),
    'can validate t3' => $workflow->can($foo, 't3'),
    'can validate t6' => $workflow->can($foo, 't6'),
    'apply t1' => clone $workflow->apply($foo, 't1'),
    'can validate t2' => $workflow->can($foo, 't2'),
    'apply t2' => clone $workflow->apply($foo, 't2'),
    'can validate t1 bis' => $workflow->can($foo, 't1'),
    'can validate t3 bis' => $workflow->can($foo, 't3'),
    'can validate t6 bis' => $workflow->can($foo, 't6'),
]);

```

The workflown:

![workflow](https://cloud.githubusercontent.com/assets/408368/14183999/4a43483c-f773-11e5-9c8b-7f157e0cb75f.png)

The output:

```
array:10 [
  "AvailableTransitions" => array:1 [
    0 => Symfony\Component\Workflow\Transition {#4
      -name: "t1"
      -froms: array:1 [
        0 => "a"
      ]
      -tos: array:2 [
        0 => "b"
        1 => "c"
      ]
    }
  ]
  "CurrentMarking" => Symfony\Component\Workflow\Marking {#19
    -places: array:1 [
      "a" => true
    ]
  }
  "can validate t1" => true
  "can validate t3" => false
  "can validate t6" => false
  "apply t1" => Symfony\Component\Workflow\Marking {#22
    -places: array:2 [
      "b" => true
      "c" => true
    ]
  }
  "apply t2" => Symfony\Component\Workflow\Marking {#47
    -places: array:1 [
      "d" => true
    ]
  }
  "can validate t1 bis" => false
  "can validate t3 bis" => true
  "can validate t6 bis" => false
]
```

Commits
-------

078e27f [Workflow] Added initial set of files
17d59a7 added the first more-or-less working version of the Workflow component
This commit is contained in:
Fabien Potencier 2016-06-23 14:39:44 +02:00
commit d36097b3ef
44 changed files with 2555 additions and 0 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

@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
use Symfony\Component\Workflow\Exception\LogicException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Definition
{
private $places = array();
private $transitions = array();
private $initialPlace;
/**
* Definition constructor.
*
* @param string[] $places
* @param Transition[] $transitions
*/
public function __construct(array $places = array(), array $transitions = array())
{
$this->addPlaces($places);
$this->addTransitions($transitions);
}
public function getInitialPlace()
{
return $this->initialPlace;
}
public function getPlaces()
{
return $this->places;
}
public function getTransitions()
{
return $this->transitions;
}
public function setInitialPlace($place)
{
if (!isset($this->places[$place])) {
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
}
$this->initialPlace = $place;
}
public function addPlace($place)
{
if (!preg_match('{^[\w\d_-]+$}', $place)) {
throw new InvalidArgumentException(sprintf('The place "%s" contains invalid characters.', $name));
}
if (!count($this->places)) {
$this->initialPlace = $place;
}
$this->places[$place] = $place;
}
public function addPlaces(array $places)
{
foreach ($places as $place) {
$this->addPlace($place);
}
}
public function addTransitions(array $transitions)
{
foreach ($transitions as $transition) {
$this->addTransition($transition);
}
}
public function addTransition(Transition $transition)
{
$name = $transition->getName();
foreach ($transition->getFroms() as $from) {
if (!isset($this->places[$from])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name));
}
}
foreach ($transition->getTos() as $to) {
if (!isset($this->places[$to])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name));
}
}
$this->transitions[$name] = $transition;
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Dumper;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
/**
* DumperInterface is the interface implemented by workflow dumper classes.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface DumperInterface
{
/**
* Dumps a workflow definition.
*
* @param Definition $definition A Definition instance
* @param Marking|null $marking A Marking instance
* @param array $options An array of options
*
* @return string The representation of the workflow
*/
public function dump(Definition $definition, Marking $marking = null, array $options = array());
}

View File

@ -0,0 +1,205 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Dumper;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
/**
* GraphvizDumper dumps a workflow as a graphviz file.
*
* You can convert the generated dot file with the dot utility (http://www.graphviz.org/):
*
* dot -Tpng workflow.dot > workflow.png
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class GraphvizDumper implements DumperInterface
{
private static $defaultOptions = array(
'graph' => array('ratio' => 'compress', 'rankdir' => 'LR'),
'node' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => true, 'width' => 1),
'edge' => array('fontsize' => 9, 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => 0.5),
);
/**
* {@inheritdoc}
*
* Dumps the workflow as a graphviz graph.
*
* Available options:
*
* * graph: The default options for the whole graph
* * node: The default options for nodes (places + transitions)
* * edge: The default options for edges
*/
public function dump(Definition $definition, Marking $marking = null, array $options = array())
{
$places = $this->findPlaces($definition, $marking);
$transitions = $this->findTransitions($definition);
$edges = $this->findEdges($definition);
$options = array_replace_recursive(self::$defaultOptions, $options);
return $this->startDot($options)
.$this->addPlaces($places)
.$this->addTransitions($transitions)
.$this->addEdges($edges)
.$this->endDot();
}
private function findPlaces(Definition $definition, Marking $marking = null)
{
$places = array();
foreach ($definition->getPlaces() as $place) {
$attributes = array();
if ($place === $definition->getInitialPlace()) {
$attributes['style'] = 'filled';
}
if ($marking && $marking->has($place)) {
$attributes['color'] = '#FF0000';
$attributes['shape'] = 'doublecircle';
}
$places[$place] = array(
'attributes' => $attributes,
);
}
return $places;
}
private function findTransitions(Definition $definition)
{
$transitions = array();
foreach ($definition->getTransitions() as $name => $transition) {
$transitions[$name] = array(
'attributes' => array('shape' => 'box', 'regular' => true),
);
}
return $transitions;
}
private function addPlaces(array $places)
{
$code = '';
foreach ($places as $id => $place) {
$code .= sprintf(" place_%s [label=\"%s\", shape=circle%s];\n",
$this->dotize($id),
$id,
$this->addAttributes($place['attributes'])
);
}
return $code;
}
private function addTransitions(array $transitions)
{
$code = '';
foreach ($transitions as $id => $place) {
$code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n",
$this->dotize($id),
$id,
$this->addAttributes($place['attributes'])
);
}
return $code;
}
private function findEdges(Definition $definition)
{
$dotEdges = array();
foreach ($definition->getTransitions() as $transition) {
foreach ($transition->getFroms() as $from) {
$dotEdges[] = array(
'from' => $from,
'to' => $transition->getName(),
'direction' => 'from',
);
}
foreach ($transition->getTos() as $to) {
$dotEdges[] = array(
'from' => $transition->getName(),
'to' => $to,
'direction' => 'to',
);
}
}
return $dotEdges;
}
private function addEdges($edges)
{
$code = '';
foreach ($edges as $edge) {
$code .= sprintf(" %s_%s -> %s_%s [style=\"solid\"];\n",
'from' === $edge['direction'] ? 'place' : 'transition',
$this->dotize($edge['from']),
'from' === $edge['direction'] ? 'transition' : 'place',
$this->dotize($edge['to'])
);
}
return $code;
}
private function startDot(array $options)
{
return sprintf("digraph workflow {\n %s\n node [%s];\n edge [%s];\n\n",
$this->addOptions($options['graph']),
$this->addOptions($options['node']),
$this->addOptions($options['edge'])
);
}
private function endDot()
{
return "}\n";
}
private function addAttributes($attributes)
{
$code = array();
foreach ($attributes as $k => $v) {
$code[] = sprintf('%s="%s"', $k, $v);
}
return $code ? ', '.implode(', ', $code) : '';
}
private function addOptions($options)
{
$code = array();
foreach ($options as $k => $v) {
$code[] = sprintf('%s="%s"', $k, $v);
}
return implode(' ', $code);
}
private function dotize($id)
{
return strtolower(preg_replace('/[^\w]/i', '_', $id));
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Event;
use Symfony\Component\EventDispatcher\Event as BaseEvent;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Transition;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Event extends BaseEvent
{
private $subject;
private $marking;
private $transition;
/**
* Event constructor.
*
* @param mixed $subject
* @param Marking $marking
* @param Transition $transition
*/
public function __construct($subject, Marking $marking, Transition $transition)
{
$this->subject = $subject;
$this->marking = $marking;
$this->transition = $transition;
}
public function getMarking()
{
return $this->marking;
}
public function getSubject()
{
return $this->subject;
}
public function getTransition()
{
return $this->transition;
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class GuardEvent extends Event
{
private $blocked = false;
public function isBlocked()
{
return $this->blocked;
}
public function setBlocked($blocked)
{
$this->blocked = (bool) $blocked;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TransitionEvent extends Event
{
private $nextState;
public function setNextState($state)
{
$this->nextState = $state;
}
public function getNextState()
{
return $this->nextState;
}
}

View File

@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class AuditTrailListener implements EventSubscriberInterface
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onLeave(Event $event)
{
foreach ($event->getTransition()->getFroms() as $place) {
$this->logger->info(sprintf('leaving "%s" for subject of class "%s"', $place, get_class($event->getSubject())));
}
}
public function onTransition(Event $event)
{
$this->logger->info(sprintf('transition "%s" for subject of class "%s"', $event->getTransition()->getName(), get_class($event->getSubject())));
}
public function onEnter(Event $event)
{
foreach ($event->getTransition()->getTos() as $place) {
$this->logger->info(sprintf('entering "%s" for subject of class "%s"', $place, get_class($event->getSubject())));
}
}
public static function getSubscribedEvents()
{
return array(
'workflow.leave' => array('onLeave'),
'workflow.transition' => array('onTransition'),
'workflow.enter' => array('onEnter'),
);
}
}

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

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Registry
{
private $workflows = array();
/**
* @param Workflow $workflow
* @param string $classname
*/
public function add(Workflow $workflow, $classname)
{
$this->workflows[] = array($workflow, $classname);
}
public function get($subject, $workflowName = null)
{
$matched = null;
foreach ($this->workflows as list($workflow, $classname)) {
if ($this->supports($workflow, $classname, $subject, $workflowName)) {
if ($matched) {
throw new InvalidArgumentException('At least two workflows match this subject. Set a different name on each and use the second (name) argument of this method.');
}
$matched = $workflow;
}
}
if (!$matched) {
throw new InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_class($subject)));
}
return $matched;
}
private function supports(Workflow $workflow, $classname, $subject, $name)
{
if (!$subject instanceof $classname) {
return false;
}
if (null === $name) {
return true;
}
return $name === $workflow->getName();
}
}

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

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Transition
{
private $name;
private $froms;
private $tos;
/**
* Transition constructor.
*
* @param string $name
* @param string|string[] $froms
* @param string|string[] $tos
*/
public function __construct($name, $froms, $tos)
{
if (!preg_match('{^[\w\d_-]+$}', $name)) {
throw new InvalidArgumentException(sprintf('The transition "%s" contains invalid characters.', $name));
}
$this->name = $name;
$this->froms = (array) $froms;
$this->tos = (array) $tos;
}
public function getName()
{
return $this->name;
}
public function getFroms()
{
return $this->froms;
}
public function getTos()
{
return $this->tos;
}
}

View File

@ -0,0 +1,274 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Workflow;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\UniqueTransitionOutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class Workflow
{
private $definition;
private $markingStore;
private $dispatcher;
private $name;
public function __construct(Definition $definition, MarkingStoreInterface $markingStore, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
{
$this->definition = $definition;
$this->markingStore = $markingStore;
$this->dispatcher = $dispatcher;
$this->name = $name;
// If the marking can contain only one place, we should control the definition
if ($markingStore instanceof UniqueTransitionOutputInterface) {
foreach ($definition->getTransitions() as $transition) {
if (1 < count($transition->getTos())) {
throw new LogicException(sprintf('The marking store (%s) of workflow "%s" can not store many places. But the transition "%s" has too many output (%d). Only one is accepted.', get_class($markingStore), $this->name, $transition->getName(), count($transition->getTos())));
}
}
}
}
/**
* Returns the object's Marking.
*
* @param object $subject A subject
*
* @return Marking The Marking
*
* @throws LogicException
*/
public function getMarking($subject)
{
$marking = $this->markingStore->getMarking($subject);
if (!$marking instanceof Marking) {
throw new LogicException(sprintf('The value returned by the MarkingStore is not an instance of "%s" for workflow "%s".', Marking::class, $this->name));
}
// check if the subject is already in the workflow
if (!$marking->getPlaces()) {
if (!$this->definition->getInitialPlace()) {
throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name));
}
$marking->mark($this->definition->getInitialPlace());
}
// check that the subject has a known place
$places = $this->definition->getPlaces();
foreach ($marking->getPlaces() as $placeName => $nbToken) {
if (!isset($places[$placeName])) {
$message = sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name);
if (!$places) {
$message .= ' It seems you forgot to add places to the current workflow.';
}
throw new LogicException($message);
}
}
// Because the marking could have been initialized, we update the subject
$this->markingStore->setMarking($subject, $marking);
return $marking;
}
/**
* Returns true if the transition is enabled.
*
* @param object $subject A subject
* @param string $transitionName A transition
*
* @return bool true if the transition is enabled
*
* @throws LogicException If the transition does not exist
*/
public function can($subject, $transitionName)
{
$transitions = $this->definition->getTransitions();
if (!isset($transitions[$transitionName])) {
throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name));
}
$transition = $transitions[$transitionName];
$marking = $this->getMarking($subject);
return $this->doCan($subject, $marking, $transition);
}
/**
* Fire a transition.
*
* @param object $subject A subject
* @param string $transitionName A transition
*
* @return Marking The new Marking
*
* @throws LogicException If the transition is not applicable
* @throws LogicException If the transition does not exist
*/
public function apply($subject, $transitionName)
{
if (!$this->can($subject, $transitionName)) {
throw new LogicException(sprintf('Unable to apply transition "%s" for workflow "%s".', $transitionName, $this->name));
}
// We can shortcut the getMarking method in order to boost performance,
// since the "can" method already checks the Marking state
$marking = $this->markingStore->getMarking($subject);
$transition = $this->definition->getTransitions()[$transitionName];
$this->leave($subject, $transition, $marking);
$this->transition($subject, $transition, $marking);
$this->enter($subject, $transition, $marking);
$this->announce($subject, $transition, $marking);
$this->markingStore->setMarking($subject, $marking);
return $marking;
}
/**
* Returns all enabled transitions.
*
* @param object $subject A subject
*
* @return Transition[] All enabled transitions
*/
public function getEnabledTransitions($subject)
{
$enabled = array();
$marking = $this->getMarking($subject);
foreach ($this->definition->getTransitions() as $transition) {
if ($this->doCan($subject, $marking, $transition)) {
$enabled[$transition->getName()] = $transition;
}
}
return $enabled;
}
public function getName()
{
return $this->name;
}
private function doCan($subject, Marking $marking, Transition $transition)
{
foreach ($transition->getFroms() as $place) {
if (!$marking->has($place)) {
return false;
}
}
if (true === $this->guardTransition($subject, $marking, $transition)) {
return false;
}
return true;
}
private function guardTransition($subject, Marking $marking, Transition $transition)
{
if (null === $this->dispatcher) {
return;
}
$event = new GuardEvent($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.guard', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.guard', $this->name), $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.guard.%s', $this->name, $transition->getName()), $event);
return $event->isBlocked();
}
private function leave($subject, Transition $transition, Marking $marking)
{
if (null !== $this->dispatcher) {
$event = new Event($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.leave', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.leave', $this->name), $event);
}
foreach ($transition->getFroms() as $place) {
$marking->unmark($place);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(sprintf('workflow.%s.leave.%s', $this->name, $place), $event);
}
}
}
private function transition($subject, Transition $transition, Marking $marking)
{
if (null === $this->dispatcher) {
return;
}
$event = new Event($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.transition', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.transition', $this->name), $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()), $event);
}
private function enter($subject, Transition $transition, Marking $marking)
{
if (null !== $this->dispatcher) {
$event = new Event($subject, $marking, $transition);
$this->dispatcher->dispatch('workflow.enter', $event);
$this->dispatcher->dispatch(sprintf('workflow.%s.enter', $this->name), $event);
}
foreach ($transition->getTos() as $place) {
$marking->mark($place);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(sprintf('workflow.%s.enter.%s', $this->name, $place), $event);
}
}
}
private function announce($subject, Transition $initialTransition, Marking $marking)
{
if (null === $this->dispatcher) {
return;
}
$event = new Event($subject, $marking, $initialTransition);
foreach ($this->definition->getTransitions() as $transition) {
if ($this->doCan($subject, $marking, $transition)) {
$this->dispatcher->dispatch(sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()), $event);
}
}
}
}

View File

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

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>