diff --git a/composer.json b/composer.json index 05c98d8318642..b4fb3a6e105f0 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php new file mode 100644 index 0000000000000..c2c5a55af954f --- /dev/null +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -0,0 +1,52 @@ + + * + * 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 + */ +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'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php new file mode 100644 index 0000000000000..d1b4e2a766d6e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @@ -0,0 +1,78 @@ + + * + * 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 + */ +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 %command.name% command dumps the graphical representation of a +workflow in DOT format + + %command.full_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); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index f38a6dd638507..57f9e29c0cf2e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -103,6 +103,7 @@ public function getConfigTreeBuilder() $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 @@ private function addProfilerSection(ArrayNodeDefinition $rootNode) ; } + 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 diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c92c083119ba9..16b5431b1acc5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -31,6 +31,7 @@ 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 @@ * @author Fabien Potencier * @author Jeremy Mikola * @author Kévin Dunglas + * @author Grégoire Pineau */ class FrameworkExtension extends Extension { @@ -129,6 +131,7 @@ public function load(array $configs, ContainerBuilder $container) $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 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ } } + /** + * 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. * diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 830702213f9e7..5cd851efbefa3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -26,6 +26,7 @@ + @@ -224,4 +225,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml new file mode 100644 index 0000000000000..d37b5d3e0670d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index e1a677daa73a5..94b6e315b8c20 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -273,6 +273,7 @@ protected static function getBundleDefaultConfig() 'directory' => '%kernel.cache_dir%/pools', 'default_redis_provider' => 'redis://localhost', ), + 'workflows' => array(), ); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow.php new file mode 100644 index 0000000000000..7f29cc385ba5b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow.php @@ -0,0 +1,30 @@ +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', + ), + ), + ), + ), + ), +)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow.xml new file mode 100644 index 0000000000000..add799b82fd44 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow.xml @@ -0,0 +1,29 @@ + + + + + + + + + property_accessor + a + a + + Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest + first + last + + + a + a + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow.yml new file mode 100644 index 0000000000000..e9eb8e1977a9d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow.yml @@ -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 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index ef0c5522fd110..0406057aad979 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -117,6 +117,13 @@ public function testDisabledProfiler() $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'); diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md new file mode 100644 index 0000000000000..c4df4750f73b2 --- /dev/null +++ b/src/Symfony/Component/Workflow/CHANGELOG.md @@ -0,0 +1,2 @@ +CHANGELOG +========= diff --git a/src/Symfony/Component/Workflow/Definition.php b/src/Symfony/Component/Workflow/Definition.php new file mode 100644 index 0000000000000..8bc1bd38ace99 --- /dev/null +++ b/src/Symfony/Component/Workflow/Definition.php @@ -0,0 +1,110 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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; + } +} diff --git a/src/Symfony/Component/Workflow/Dumper/DumperInterface.php b/src/Symfony/Component/Workflow/Dumper/DumperInterface.php new file mode 100644 index 0000000000000..b0eebd34f1952 --- /dev/null +++ b/src/Symfony/Component/Workflow/Dumper/DumperInterface.php @@ -0,0 +1,35 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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()); +} diff --git a/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php new file mode 100644 index 0000000000000..56bbef64aded4 --- /dev/null +++ b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php @@ -0,0 +1,205 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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)); + } +} diff --git a/src/Symfony/Component/Workflow/Event/Event.php b/src/Symfony/Component/Workflow/Event/Event.php new file mode 100644 index 0000000000000..a690b2b330e7a --- /dev/null +++ b/src/Symfony/Component/Workflow/Event/Event.php @@ -0,0 +1,58 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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; + } +} diff --git a/src/Symfony/Component/Workflow/Event/GuardEvent.php b/src/Symfony/Component/Workflow/Event/GuardEvent.php new file mode 100644 index 0000000000000..bf4b6f3971e7a --- /dev/null +++ b/src/Symfony/Component/Workflow/Event/GuardEvent.php @@ -0,0 +1,31 @@ + + * + * 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 + * @author Grégoire Pineau + */ +class GuardEvent extends Event +{ + private $blocked = false; + + public function isBlocked() + { + return $this->blocked; + } + + public function setBlocked($blocked) + { + $this->blocked = (bool) $blocked; + } +} diff --git a/src/Symfony/Component/Workflow/Event/TransitionEvent.php b/src/Symfony/Component/Workflow/Event/TransitionEvent.php new file mode 100644 index 0000000000000..1fe0e2ff962a9 --- /dev/null +++ b/src/Symfony/Component/Workflow/Event/TransitionEvent.php @@ -0,0 +1,30 @@ + + * + * 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 + */ +class TransitionEvent extends Event +{ + private $nextState; + + public function setNextState($state) + { + $this->nextState = $state; + } + + public function getNextState() + { + return $this->nextState; + } +} diff --git a/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php b/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php new file mode 100644 index 0000000000000..a39c89d18c3cb --- /dev/null +++ b/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php @@ -0,0 +1,57 @@ + + * + * 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 + */ +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'), + ); + } +} diff --git a/src/Symfony/Component/Workflow/Exception/ExceptionInterface.php b/src/Symfony/Component/Workflow/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..b0dfa9b79bbc1 --- /dev/null +++ b/src/Symfony/Component/Workflow/Exception/ExceptionInterface.php @@ -0,0 +1,20 @@ + + * + * 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 + * @author Grégoire Pineau + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Workflow/Exception/InvalidArgumentException.php b/src/Symfony/Component/Workflow/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..c44fa05cdd05c --- /dev/null +++ b/src/Symfony/Component/Workflow/Exception/InvalidArgumentException.php @@ -0,0 +1,20 @@ + + * + * 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 + * @author Grégoire Pineau + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Workflow/Exception/LogicException.php b/src/Symfony/Component/Workflow/Exception/LogicException.php new file mode 100644 index 0000000000000..d0cf09f9dfe63 --- /dev/null +++ b/src/Symfony/Component/Workflow/Exception/LogicException.php @@ -0,0 +1,20 @@ + + * + * 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 + * @author Grégoire Pineau + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Workflow/LICENSE b/src/Symfony/Component/Workflow/LICENSE new file mode 100644 index 0000000000000..39fa189d2b5fc --- /dev/null +++ b/src/Symfony/Component/Workflow/LICENSE @@ -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. diff --git a/src/Symfony/Component/Workflow/Marking.php b/src/Symfony/Component/Workflow/Marking.php new file mode 100644 index 0000000000000..0d8bab25b7fe3 --- /dev/null +++ b/src/Symfony/Component/Workflow/Marking.php @@ -0,0 +1,52 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Workflow/MarkingStore/MarkingStoreInterface.php b/src/Symfony/Component/Workflow/MarkingStore/MarkingStoreInterface.php new file mode 100644 index 0000000000000..e73c9eb596c62 --- /dev/null +++ b/src/Symfony/Component/Workflow/MarkingStore/MarkingStoreInterface.php @@ -0,0 +1,39 @@ + + * + * 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 + */ +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); +} diff --git a/src/Symfony/Component/Workflow/MarkingStore/PropertyAccessorMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/PropertyAccessorMarkingStore.php new file mode 100644 index 0000000000000..faf1e8a6c4024 --- /dev/null +++ b/src/Symfony/Component/Workflow/MarkingStore/PropertyAccessorMarkingStore.php @@ -0,0 +1,56 @@ + + * + * 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 + */ +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()); + } +} diff --git a/src/Symfony/Component/Workflow/MarkingStore/ScalarMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/ScalarMarkingStore.php new file mode 100644 index 0000000000000..949661ee10627 --- /dev/null +++ b/src/Symfony/Component/Workflow/MarkingStore/ScalarMarkingStore.php @@ -0,0 +1,62 @@ + + * + * 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 + */ +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())); + } +} diff --git a/src/Symfony/Component/Workflow/MarkingStore/UniqueTransitionOutputInterface.php b/src/Symfony/Component/Workflow/MarkingStore/UniqueTransitionOutputInterface.php new file mode 100644 index 0000000000000..35c00eb58d101 --- /dev/null +++ b/src/Symfony/Component/Workflow/MarkingStore/UniqueTransitionOutputInterface.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +interface UniqueTransitionOutputInterface +{ +} diff --git a/src/Symfony/Component/Workflow/README.md b/src/Symfony/Component/Workflow/README.md new file mode 100644 index 0000000000000..a2bf37aa7f3c4 --- /dev/null +++ b/src/Symfony/Component/Workflow/README.md @@ -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) diff --git a/src/Symfony/Component/Workflow/Registry.php b/src/Symfony/Component/Workflow/Registry.php new file mode 100644 index 0000000000000..2f94288ebb9c5 --- /dev/null +++ b/src/Symfony/Component/Workflow/Registry.php @@ -0,0 +1,65 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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(); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/DefinitionTest.php b/src/Symfony/Component/Workflow/Tests/DefinitionTest.php new file mode 100644 index 0000000000000..4a4465fd64a10 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/DefinitionTest.php @@ -0,0 +1,73 @@ +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'))); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php b/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php new file mode 100644 index 0000000000000..d6e9cd30f31d9 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/Dumper/GraphvizDumperTest.php @@ -0,0 +1,203 @@ +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"]; +} +'; + } +} diff --git a/src/Symfony/Component/Workflow/Tests/EventListener/AuditTrailListenerTest.php b/src/Symfony/Component/Workflow/Tests/EventListener/AuditTrailListenerTest.php new file mode 100644 index 0000000000000..0422009f0ee24 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/EventListener/AuditTrailListenerTest.php @@ -0,0 +1,54 @@ +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; + } +} diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertyAccessorMarkingStoreTest.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertyAccessorMarkingStoreTest.php new file mode 100644 index 0000000000000..557a241689ece --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertyAccessorMarkingStoreTest.php @@ -0,0 +1,32 @@ +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); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/ScalarMarkingStoreTest.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/ScalarMarkingStoreTest.php new file mode 100644 index 0000000000000..df8d748a0d927 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/ScalarMarkingStoreTest.php @@ -0,0 +1,32 @@ +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); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/MarkingTest.php b/src/Symfony/Component/Workflow/Tests/MarkingTest.php new file mode 100644 index 0000000000000..026ca607bee13 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/MarkingTest.php @@ -0,0 +1,35 @@ + 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()); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/RegistryTest.php b/src/Symfony/Component/Workflow/Tests/RegistryTest.php new file mode 100644 index 0000000000000..719886dd1c03f --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/RegistryTest.php @@ -0,0 +1,74 @@ +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 +{ +} diff --git a/src/Symfony/Component/Workflow/Tests/TransitionTest.php b/src/Symfony/Component/Workflow/Tests/TransitionTest.php new file mode 100644 index 0000000000000..fbf9b38b23bb6 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/TransitionTest.php @@ -0,0 +1,26 @@ +assertSame('name', $transition->getName()); + $this->assertSame(array('a'), $transition->getFroms()); + $this->assertSame(array('b'), $transition->getTos()); + } +} diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php new file mode 100644 index 0000000000000..58e9ecc9ead15 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php @@ -0,0 +1,288 @@ +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) + { + } +} diff --git a/src/Symfony/Component/Workflow/Transition.php b/src/Symfony/Component/Workflow/Transition.php new file mode 100644 index 0000000000000..30cc5eca47d82 --- /dev/null +++ b/src/Symfony/Component/Workflow/Transition.php @@ -0,0 +1,60 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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; + } +} diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php new file mode 100644 index 0000000000000..0bf637e3d7ebf --- /dev/null +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -0,0 +1,274 @@ + + * + * 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 + * @author Grégoire Pineau + */ +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); + } + } + } +} diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json new file mode 100644 index 0000000000000..3f12343dc78a1 --- /dev/null +++ b/src/Symfony/Component/Workflow/composer.json @@ -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" + } + } +} diff --git a/src/Symfony/Component/Workflow/phpunit.xml.dist b/src/Symfony/Component/Workflow/phpunit.xml.dist new file mode 100644 index 0000000000000..5817db3b8fe26 --- /dev/null +++ b/src/Symfony/Component/Workflow/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy