Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
| // Marking must have ONE place | ||
| $preCondition =false; | ||
| foreach ($transition->getFroms()as$place) { | ||
| $preCondition =$preCondition ||$marking->has($place); |
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.
You can break as soon as you get atrue ?!
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.
Correct. I'll update
stof commentedAug 16, 2016
shouldn't state machines be implemented with transitions having a single starting place, instead of making a OR between places, which changes the meaning of the transition ? I'm quite sure that your state machine doing a OR between starting places of a given transition should actually be implemented as several transitions with a single starting place each. |
Nyholm commentedAug 16, 2016
Thank you for the feedback and for challenging the theory behind this. I appreciate that.
(I assume that "starting place"="input place" and not "initial place") // Example 1 Could be considered the same as// a -> t1 -> b a -> t1 -> b// ↑ ↑// c -> t1 -> ⅃ c // Example 2// a -> t1 -> b//// c -> t1 -> d The output state for Here is an example of the doors of an elevator (sourceWikipedia)
Yes, it does a OR but as stated above that is okey. The alternative implementation of this is to rewrite Rewriting |
stof commentedAug 16, 2016
Well, in your state machine, I consider that you have multiple transitions being labelled as IMO, requiring transition names to be unique is the actual issue in your case. Your proposal is actually changing the Workflow component to implement a weird thing. Your state machine above indeed has 6 transitions in your schema above |
Nyholm commentedAug 16, 2016
You may be right. I have taken a shortcut here. We discussed non-unique transition names in#19605 as well. I'll see if I can find a better solution. |
ogizanagi commentedAug 16, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Nyholm commentedAug 16, 2016
I've update the PR now. I've made sure that the transition name can be non-unique. There is no shortcut, there is actually no decreased performance (I was wrong before) and the model is a better mapping of the state machine. Please give it another review. Note: This will be a BC break because |
lyrixx commentedAug 17, 2016
Hello. Unfortunately the transition name in a petri name should be unique. Anyway, it's simpler to add a new attribute |
Nyholm commentedAug 17, 2016
Hey. I can't find any documentations about petri nets that says that the transition name has to be unique. Can you provide a reference?
So the Transition::name will be an unique id and the Transition::title would be... used as an id when you do |
lyrixx commentedAug 17, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
no, with |
Nyholm commentedAug 17, 2016
Then what is the point of adding a If I still need the full try {$workflow->apply($elevator,'close_if_opened');}catch ( .. ) {try {$workflow->apply($elevator,'close_if_opening'); }catch (...) { ... } }
I have not found anything yet. Have you had any success? |
lyrixx commentedAug 17, 2016
the point of adding a title is you hide the potential complexity of the workflow. You display to the end user a simple action like close. Then, because some transitions will not be available, the workflow will display only the close (close_if_opened) transition. and so you never had to hardcode close_if_opened. Do you see what i'm trying to say ? If you can, I show you in with the example on my repo.
Inhttps://en.wikipedia.org/wiki/Petri_net#Formal_definition_and_basic_terminology:
And a set is a:
|
Nyholm commentedAug 17, 2016
I see what you mean by using a title. In your case it does make sense. But in my application I do not show the places/states to the end users. For example: I use a state machine to determent what emails to send. I do something like: if ($workflow->can($object,'send_type1')) {// Do send$workflow->apply(...)} "send_type1" could move the state from
Yes, T is a set of disjoint objects. But it does not say anything about the name/title attributes of those objects that must be unique. In fact, the name of the object has nothing to do with the mathematical model. |
lyrixx commentedAug 17, 2016
About your exemple, you can simply use $workflow->getEnabledTransitions to get the list of all available transition ?
I guess I / You miss something because to me it's clear it should be unique. |
Nyholm commentedAug 17, 2016
Yeah, but Im not interested in getting a list what is enabled and then parse that list. I know the name of the action I want to do. I just want to check if I can do it or not. Agreed. I think we intrepid the definition differently. For be it is clear that the transitions should be separate objects but the attributes of those objects do not have any restrictions. Do you see any issues with the workflow net that two transitions share the same name? |
lyrixx commentedAug 17, 2016
Right now, no. But I have a bad feeling about it. :/ |
Nyholm commentedAug 17, 2016
Please elaborate. Im happy to challenge this PR. This PR brings more flexibility. Wecould make sure to restrict us by forcing the transitions names to be unique whenever we add transition to the |
lyrixx commentedAug 23, 2016
(One week later) I still don't know what to think about this PR. It looks good to me but I'm still a bit afraid of potential side effect that I'm not able to see now. @stof What do you think? |
stof commentedAug 23, 2016
@lyrixx Well, transition names must be unique among transition sharing a starting place (otherwise asking to perform a transition would be ambiguous). But I don't see a need to make them unique in the whole graph (except maybe because of your feature about event being dispatched as you use the name in them, but the impact should be evaluated). Regarding this PR, what I don't like is that it changes totally what a transition means in the workflow depending on some other thing. We should not have a If we want to be able to enforce a state machine workflow, we should probably have a feature validating a workflow instance to enforce some rules. Here are a few ideas about what can be validated at this level:
|
fabpot commentedSep 14, 2016
So, what do we do here? |
Nyholm commentedSep 15, 2016
I feel that we all want to support state machines. The question is HOW. I like the idea by@stof to enforce some rules. What if we introduced a StateMachine class. class PetriNet {// ...}class Workflowextends PetriNet {publicfunction__construct($definition, ...) {// Validate that the definition is a valid Workflow }}class StateMachineextends PetriNet (Or WorkFlow) {publicfunction__construct($definition, ...) {// Validate that the definition is a valid StateMachine }} This would enforce the rules, remove the need for |
lyrixx commentedSep 19, 2016
Indeed I would be nice to support state machine out of the box. But I don't really like the current implementation, and I totally approve stof's comment. It looks like the previous comment could be a better approach. |
Nyholm commentedSep 27, 2016
I introduced the PetriNet and StateMachine classes. Since StateMachines and Workflows are both subtypes of PetriNet I made sure our classes reflects that. The rules@stof suggests are enforced in the constructors. I would like to have some feedback before I continue with this PR. Questions:
TODO:
|
stof left a comment
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.
I'm not sure this should be done in the class constructor. This means that each request in prod would run the validation of the workflow consistency again, even when the workflow is defined in the config (and so cannot change between deployments.
IMO, it would be better to have a way to validate a workflow as being a state machine externally. This way, you can have the validation running only when necessary (i.e. when changing your workflow) instead of doing it on each request.
Btw, even if we keep this constructor-based validation, I would simplify the class structure. A state machine is a workflow, so it should extend Workflow. And then, we don't need the PetriNet class anymore (the component doesnot implement PetriNet. It only implements the workflow subset of it, as transitions cannot be weighted)
| Definition$definition, | ||
| MarkingStoreInterface$markingStore, | ||
| EventDispatcherInterface$dispatcher =null, | ||
| $name ='unnamed' |
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 signature must stay on a single line
| // If the marking can contain only one place, we should control the definition | ||
| if ($markingStoreinstanceof UniqueTransitionOutputInterface) { | ||
| /** @var Transition $transition */ |
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.
this is useless if we properly document the return type ofgetTransitions asTransition[], so please remove this inline phpdoc
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
| $transitionFromNames =array(); | ||
| /** @var Transition $transition */ |
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.
useless phpdoc
| { | ||
| publicfunction__construct(Definition$definition,MarkingStoreInterface$markingStore =null,EventDispatcherInterface$dispatcher =null,$name ='unnamed') | ||
| { | ||
| if (!$markingStore) { |
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.
does it really make sense to be optional ?
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.
Maybe not
| parent::__construct($definition,$markingStore,$dispatcher,$name); | ||
| // If the marking can contain only one place, we should control the definition | ||
| if ($markingStoreinstanceof UniqueTransitionOutputInterface) { |
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.
do we still need this interface ? It looks like a partial attempt to implement state machines IMO
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.
I did not introduce that interface. =)
Depending on the MarkinStore we can or cannot be in two places at the same time.
| $workflowDefinition =newDefinitionDecorator('workflow.abstract'); | ||
| $type =$workflow['type']; | ||
| $workflowDefinition =newDefinitionDecorator($type.'.abstract'); |
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.
I would usesprintf for all string concatenation, to be consistent.
javiereguiluz commentedNov 7, 2016
@rvanlaak we are supper late in the Symfony 3.2 cycle (the final release is in around 20 days) so we can only merge new features if they look rock-solid, they are not huge and there is an unanimous agreement about it. |
Nyholm commentedNov 7, 2016
Is this such PR? |
fabpot commentedNov 7, 2016
To be clear, the only options are: reverting the Workflow component for 3.2 and wait for 3.3 to reintroduce it or merge this PR for 3.2. |
Nyholm commentedNov 7, 2016
Okey. Good. Im fine with either of those. But I would appreciate (of course) if the workflow component was introduced in 3.2. |
rufinus commentedNov 7, 2016
Even if i dont have a saying in this. I really looked forward to the workflow component, but without the state-machine extension it is just not usable (and i bet the most common use case for your userbase will be the state-machine). The only thing needed for 3.2 is the changed return value (which would be a BC break after 3.2). So the state-machine itself could be added in a 3.2.1. Of course, i would really love to see this in 3.2.0. But if I need to wait for 3.3.0 (which is 6 months away) I'm forced to use a different integration. |
rvanlaak commentedNov 7, 2016
Is there a list of actionables for this PR to be merged already?
Ghehe, that won't happen ;) |
stof commentedNov 7, 2016
@rufinus new features are never added in patch releases |
fabpot commentedNov 7, 2016
@lyrixx I think we're waiting for your final approval here :) |
lyrixx commentedNov 7, 2016
Here we go ;) First, thanks a lot for your work on this PR. It's really cool. For me the design is good and we could merge this one in master (3.2) I tested this PR against my demo app, and I also tested the new state machine. Everything is in thenew branch.Warning: If you want to tests this project, you have tomanually updated symfony vendor to use this PR. Finally, I made some modification in this PR to enhance few things. I have attached the patch to this PR. patch: commit ef075134f64a9b31e4d4c7ab7fc14d6c37b172bbAuthor: Grégoire Pineau <lyrixx@lyrixx.info>Date: Mon Nov 7 18:57:54 2016 +0100 Patchdiff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.phpindex 4f1024e..09ad3dd 100644--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php@@ -35,13 +35,13 @@ class ValidateWorkflowsPass implements CompilerPassInterface foreach ($taggedServices as $id => $tags) { $definition = $container->get($id); foreach ($tags as $tag) {- if (empty($tag['name'])) {+ 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 (empty($tag['type'])) {+ 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 (empty($tag['marking_store'])) {+ 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)); }diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.phpindex 3a1f0f0..b7cdb63 100644--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php@@ -425,7 +425,7 @@ class FrameworkExtension extends Extension foreach ($workflow['marking_store']['arguments'] as $argument) { $markingStoreDefinition->addArgument($argument); }- } else {+ } elseif (isset($workflow['marking_store']['service'])) { $markingStoreDefinition = new Reference($workflow['marking_store']['service']); }@@ -438,7 +438,9 @@ class FrameworkExtension extends Extension $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 = sprintf('%s.%s', $type, $name);diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xmlindex cb8c132..1314be8 100644--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/workflow.xml@@ -7,13 +7,13 @@ <services> <service abstract="true"> <argument /> <!-- workflow definition -->- <argument /> <!-- marking store -->+ <argument type="constant">null</argument> <!-- marking store --> <argument type="service" on-invalid="ignore" /> <argument /> <!-- name --> </service> <service abstract="true"> <argument /> <!-- workflow definition -->- <argument /> <!-- marking store -->+ <argument type="constant">null</argument> <!-- marking store --> <argument type="service" on-invalid="ignore" /> <argument /> <!-- name --> </service>diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.phpindex 6144de1..4a7ea56 100644--- a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php+++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php@@ -113,7 +113,9 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase $subject = new \stdClass(); $subject->marking = null; $eventDispatcher = new EventDispatcher();- $eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) { $event->setBlocked(true); });+ $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'));@@ -188,7 +190,9 @@ class WorkflowTest extends \PHPUnit_Framework_TestCase $subject = new \stdClass(); $subject->marking = null; $eventDispatcher = new EventDispatcher();- $eventDispatcher->addListener('workflow.workflow_name.guard.t1', function (GuardEvent $event) { $event->setBlocked(true); });+ $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));diff --git a/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php b/src/Symfony/Component/Workflow/Validator/StateMachineValidator.phpindex 57dc632..6f34c1f 100644--- a/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php+++ b/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php@@ -53,7 +53,7 @@ class StateMachineValidator implements DefinitionValidatorInterface if (isset($transitionFromNames[$from][$transition->getName()])) { throw new InvalidDefinitionException( sprintf(- 'A transition from a place/state must have an unique name. Multiple transition named "%s" from place/state "%s" where found on StateMachine "%s". ',+ 'A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" where found on StateMachine "%s". ', $transition->getName(), $from, $namediff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.phpindex 4e83778..348b3b9 100644--- a/src/Symfony/Component/Workflow/Workflow.php+++ b/src/Symfony/Component/Workflow/Workflow.php@@ -16,6 +16,7 @@ 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\PropertyAccessorMarkingStore; /** * @author Fabien Potencier <fabien@symfony.com>@@ -29,10 +30,10 @@ class Workflow private $dispatcher; private $name;- public function __construct(Definition $definition, MarkingStoreInterface $markingStore, EventDispatcherInterface $dispatcher = null, $name = 'unnamed')+ public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, $name = 'unnamed') { $this->definition = $definition;- $this->markingStore = $markingStore;+ $this->markingStore = $markingStore ?: new PropertyAccessorMarkingStore(); $this->dispatcher = $dispatcher; $this->name = $name; }@@ -163,7 +164,7 @@ class Workflow * @param Marking $marking * @param Transition $transition *- * @return bool|void boolean true if this transition is guarded, ie you cannot use it.+ * @return bool|void boolean true if this transition is guarded, ie you cannot use it */ private function guardTransition($subject, Marking $marking, Transition $transition) {@@ -253,25 +254,21 @@ class Workflow { $transitions = $this->definition->getTransitions();- $namedTransitions = array_filter(- $transitions,- function (Transition $transition) use ($transitionName) {- return $transitionName === $transition->getName();- }- );+ $transitions = array_filter($transitions, function (Transition $transition) use ($transitionName) {+ return $transitionName === $transition->getName();+ });- if (empty($namedTransitions)) {- throw new LogicException(- sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name)- );+ if (!$transitions) {+ throw new LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transitionName, $this->name)); }- return $namedTransitions;+ return $transitions; } /**- * Return the first Transition in $transitions that is valid for the $subject and $marking. null is returned when- * you cannot do any Transition in $transitions on the $subject.+ * Return the first Transition in $transitions that is valid for the+ * $subject and $marking. null is returned when you cannot do any Transition+ * in $transitions on the $subject. * * @param object $subject * @param Marking $marking@@ -292,7 +289,5 @@ class Workflow return $transition; } }-- return; } } |
Nyholm commentedNov 7, 2016
Thank you. Excellent news. =) Do you want to apply the patch your self (I've given you permissions) or do you want me to do it now? |
lyrixx commentedNov 7, 2016
@Nyholm Thanks. I just pushed my patch in your fork. And so my commit just land in this pr ;) 👍 |
lyrixx commentedNov 7, 2016
I just rebased the PR and squashed all your commits. |
Nyholm commentedNov 7, 2016
Thank you |
fabpot commentedNov 7, 2016
Thank you@Nyholm. |
…yholm, lyrixx)This PR was merged into the 3.2-dev branch.Discussion----------[Workflow] Make the Workflow support State Machines| Q | A || --- | --- || Branch? | "master" || Bug fix? | no || New feature? | yes || BC breaks? | yes, getEnabledTransistions does not return an assoc array. || Deprecations? | no || Tests pass? | yes || Fixed tickets |Fixes#19605,Closes#19607 || License | MIT || Doc PR |symfony/symfony-docs#6871 |While researching for the docs of the component I've found that:- A Workflow is a subclass of a Petri net- A state machine is subclass of a Workflow- A state machine must not be in many places simultaneously.This PR adds a new interface to the marking store that allow us to validate the transition to true if ANY _input_ (froms) place matches the _tokens_ (marking). The default behavior is that ALL input places must match the tokens.Commits-------9e49198 [Workflow] Made the code more robbust and ease on-boardingbdd3f95 Make the Workflow support State Machines
Nyholm commentedNov 7, 2016
Thank you all for the feedback! You are awesome! |
rufinus commentedNov 9, 2016
@Nyholm is it a wanted effect that no services a created for the state-machines? |
Nyholm commentedNov 9, 2016
No you should be able to get both workflows and state machines from the service container. Let's move this conversation to the doc PR. It needs some input. |

Uh oh!
There was an error while loading.Please reload this page.
While researching for the docs of the component I've found that:
This PR adds a new interface to the marking store that allow us to validate the transition to true if ANYinput (froms) place matches thetokens (marking). The default behavior is that ALL input places must match the tokens.