Skip to content

[Workflow] Make the Workflow support State Machines #19629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?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\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface;
use Symfony\Component\Workflow\Validator\SinglePlaceWorkflowValidator;
use Symfony\Component\Workflow\Validator\StateMachineValidator;
use Symfony\Component\Workflow\Validator\WorkflowValidator;

/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class ValidateWorkflowsPass implements CompilerPassInterface
{
/**
* @var DefinitionValidatorInterface[]
*/
private $validators = array();

public function process(ContainerBuilder $container)
{
$taggedServices = $container->findTaggedServiceIds('workflow.definition');
foreach ($taggedServices as $id => $tags) {
$definition = $container->get($id);
foreach ($tags as $tag) {
if (!array_key_exists('name', $tag)) {
throw new RuntimeException(sprintf('The "name" for the tag "workflow.definition" of service "%s" must be set.', $id));
}
if (!array_key_exists('type', $tag)) {
throw new RuntimeException(sprintf('The "type" for the tag "workflow.definition" of service "%s" must be set.', $id));
}
if (!array_key_exists('marking_store', $tag)) {
throw new RuntimeException(sprintf('The "marking_store" for the tag "workflow.definition" of service "%s" must be set.', $id));
}

$this->getValidator($tag)->validate($definition, $tag['name']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the key name in $tag could be undefined ?!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. You are correct. I'll throw exception of some data is missing on the tag.

}
}
}

/**
* @param array $tag
*
* @return DefinitionValidatorInterface
*/
private function getValidator($tag)
{
if ($tag['type'] === 'state_machine') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here ?!

$name = 'state_machine';
$class = StateMachineValidator::class;
} elseif ($tag['marking_store'] === 'scalar') {
$name = 'single_place';
$class = SinglePlaceWorkflowValidator::class;
} else {
$name = 'workflow';
$class = WorkflowValidator::class;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this logic be better?


if (empty($this->validators[$name])) {
$this->validators[$name] = new $class();
}

return $this->validators[$name];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->useAttributeAsKey('name')
->prototype('array')
->children()
->enumNode('type')
->values(array('workflow', 'state_machine'))
->defaultValue('workflow')
->end()
->arrayNode('marking_store')
->isRequired()
->children()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,28 +404,48 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde
$registryDefinition = $container->getDefinition('workflow.registry');

foreach ($workflows as $name => $workflow) {
$type = $workflow['type'];

$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 ($type === 'workflow') {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $transition['from'], $transition['to']))));
} elseif ($type === 'state_machine') {
foreach ($transition['from'] as $from) {
foreach ($transition['to'] as $to) {
$definitionDefinition->addMethodCall('addTransition', array(new Definition(Workflow\Transition::class, array($transitionName, $from, $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 {
} elseif (isset($workflow['marking_store']['service'])) {
$markingStoreDefinition = new Reference($workflow['marking_store']['service']);
}

$workflowDefinition = new DefinitionDecorator('workflow.abstract');
$definitionDefinition->addTag('workflow.definition', array(
'name' => $name,
'type' => $type,
'marking_store' => isset($workflow['marking_store']['type']) ? $workflow['marking_store']['type'] : null,
));
$definitionDefinition->setPublic(false);
Copy link
Member Author

@Nyholm Nyholm Nov 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been playing with this implementation a bit. I think I would like to have the definition to be public. Otherwise I can't dump my graph nor validate it again (for whatever reason)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, Having the definition public is a an issue, because it's mutable. So people could change the definition at runtime and cause lot of trouble.
Anyway, I don't have a better way to do it. Or may be with the reflection... WTDY?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's right. Interesting. I was considering if it would be a feature to have it public and mutable. But it isnt, If you want to modify the workflow you should do it with guards.

I think we should leave this private for now to be able to postpone a solution for later. I also think we should introduce a DefinitionInterface with:

  • getInitialPlace()
  • getPlaces()
  • getTransitions()

That would allow us to have an ImmutableDefinition class if we later decide that is the better solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it isnt, If you want to modify the workflow you should do it with guards.

IMHO you should never modify a workflow. What is the use case you have in mind?

Copy link
Member Author

@Nyholm Nyholm Nov 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, You should never modify a workflow's definition. But you could modify the transition's behavior with the workflow.guard event. But that is something you already implemented.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


$workflowDefinition = new DefinitionDecorator(sprintf('%s.abstract', $type));
$workflowDefinition->replaceArgument(0, $definitionDefinition);
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
if (isset($markingStoreDefinition)) {
$workflowDefinition->replaceArgument(1, $markingStoreDefinition);
}
$workflowDefinition->replaceArgument(3, $name);

$workflowId = 'workflow.'.$name;
$workflowId = sprintf('%s.%s', $type, $name);

$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
$container->setDefinition($workflowId, $workflowDefinition);

foreach ($workflow['supports'] as $supportedClass) {
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ValidateWorkflowsPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
Expand Down Expand Up @@ -93,6 +94,7 @@ public function build(ContainerBuilder $container)
$container->addCompilerPass(new PropertyInfoPass());
$container->addCompilerPass(new ControllerArgumentValueResolverPass());
$container->addCompilerPass(new CachePoolPass());
$container->addCompilerPass(new ValidateWorkflowsPass());
$container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING);

if ($container->getParameter('kernel.debug')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
<services>
<service id="workflow.abstract" class="Symfony\Component\Workflow\Workflow" abstract="true">
<argument /> <!-- workflow definition -->
<argument /> <!-- marking store -->
<argument type="constant">null</argument> <!-- marking store -->
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
<argument /> <!-- name -->
</service>
<service id="state_machine.abstract" class="Symfony\Component\Workflow\StateMachine" abstract="true">
<argument /> <!-- workflow definition -->
<argument type="constant">null</argument> <!-- marking store -->
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
<argument /> <!-- name -->
</service>
Expand Down
5 changes: 4 additions & 1 deletion src/Symfony/Component/Workflow/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public function getPlaces()
return $this->places;
}

/**
* @return Transition[]
*/
public function getTransitions()
{
return $this->transitions;
Expand Down Expand Up @@ -103,6 +106,6 @@ public function addTransition(Transition $transition)
}
}

$this->transitions[$name] = $transition;
$this->transitions[] = $transition;
}
}
11 changes: 6 additions & 5 deletions src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ private function findTransitions(Definition $definition)
{
$transitions = array();

foreach ($definition->getTransitions() as $name => $transition) {
$transitions[$name] = array(
foreach ($definition->getTransitions() as $transition) {
$transitions[] = array(
'attributes' => array('shape' => 'box', 'regular' => true),
'name' => $transition->getName(),
);
}

Expand All @@ -111,10 +112,10 @@ private function addTransitions(array $transitions)
{
$code = '';

foreach ($transitions as $id => $place) {
foreach ($transitions as $place) {
$code .= sprintf(" transition_%s [label=\"%s\", shape=box%s];\n",
$this->dotize($id),
$id,
$this->dotize($place['name']),
$place['name'],
$this->addAttributes($place['attributes'])
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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\Exception;

/**
* Thrown by the DefinitionValidatorInterface when the definition is invalid.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class InvalidDefinitionException extends \LogicException implements ExceptionInterface
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ScalarMarkingStore implements MarkingStoreInterface, UniqueTransitionOutputInterface
class ScalarMarkingStore implements MarkingStoreInterface
{
private $property;
private $propertyAccessor;
Expand Down

This file was deleted.

18 changes: 18 additions & 0 deletions src/Symfony/Component/Workflow/StateMachine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Symfony\Component\Workflow;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;

/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class StateMachine extends Workflow
{
public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')
{
parent::__construct($definition, $markingStore ?: new ScalarMarkingStore(), $dispatcher, $name);
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/Workflow/Tests/DefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function testAddTransition()
$definition = new Definition($places, array($transition));

$this->assertCount(1, $definition->getTransitions());
$this->assertSame($transition, $definition->getTransitions()['name']);
$this->assertSame($transition, $definition->getTransitions()[0]);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/Symfony/Component/Workflow/Tests/RegistryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ protected function setUp()

$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);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow1'), Subject1::class);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow2'), Subject2::class);
$this->registry->add(new Workflow(new Definition(), $this->getMockBuilder(MarkingStoreInterface::class)->getMock(), $this->getMockBuilder(EventDispatcherInterface::class)->getMock(), 'workflow3'), Subject2::class);
}

protected function tearDown()
Expand Down Expand Up @@ -55,7 +55,7 @@ public function testGetWithMultipleMatch()
}

/**
* @expectedException Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedException \Symfony\Component\Workflow\Exception\InvalidArgumentException
* @expectedExceptionMessage Unable to find a workflow for class "stdClass".
*/
public function testGetWithNoMatch()
Expand Down
75 changes: 75 additions & 0 deletions src/Symfony/Component/Workflow/Tests/StateMachineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Symfony\Component\Workflow\Tests;

use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\StateMachine;
use Symfony\Component\Workflow\Transition;

class StateMachineTest extends \PHPUnit_Framework_TestCase
{
public function testCan()
{
$places = array('a', 'b', 'c', 'd');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t1', 'd', 'b');
$transitions[] = new Transition('t2', 'b', 'c');
$transitions[] = new Transition('t3', 'b', 'd');
$definition = new Definition($places, $transitions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a ASCII representation of theses StateMachine (like I did with my fixtures)?
IIRC, I used graphviz dumper + dot, but I'm not sure.


$net = new StateMachine($definition);
$subject = new \stdClass();

// If you are in place "a" you should be able to apply "t1"
$subject->marking = 'a';
$this->assertTrue($net->can($subject, 't1'));
$subject->marking = 'd';
$this->assertTrue($net->can($subject, 't1'));

$subject->marking = 'b';
$this->assertFalse($net->can($subject, 't1'));

// The graph looks like:
//
// +-------------------------------+
// v |
// +---+ +----+ +----+ +----+ +---+ +----+
// | a | --> | t1 | --> | b | --> | t3 | --> | d | --> | t1 |
// +---+ +----+ +----+ +----+ +---+ +----+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}

public function testCanWithMultipleTransition()
{
$places = array('a', 'b', 'c');
$transitions[] = new Transition('t1', 'a', 'b');
$transitions[] = new Transition('t2', 'a', 'c');
$definition = new Definition($places, $transitions);

$net = new StateMachine($definition);
$subject = new \stdClass();

// If you are in place "a" you should be able to apply "t1" and "t2"
$subject->marking = 'a';
$this->assertTrue($net->can($subject, 't1'));
$this->assertTrue($net->can($subject, 't2'));

// The graph looks like:
//
// +----+ +----+ +---+
// | a | --> | t1 | --> | b |
// +----+ +----+ +---+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}
}
Loading