-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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']); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param array $tag | ||
* | ||
* @return DefinitionValidatorInterface | ||
*/ | ||
private function getValidator($tag) | ||
{ | ||
if ($tag['type'] === 'state_machine') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
That would allow us to have an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IMHO you should never modify a workflow. What is the use case you have in mind? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See suggestion here: https://github.com/Nyholm/symfony/pull/1/files |
||
|
||
$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) { | ||
|
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 | ||
{ | ||
} |
This file was deleted.
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); | ||
} | ||
} |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? |
||
|
||
$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 | | ||
// +----+ +----+ | ||
} | ||
} |
There was a problem hiding this comment.
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 ?!There was a problem hiding this comment.
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.