-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
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 a true
?!
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
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. |
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 (source Wikipedia)
Yes, it does a OR but as stated above that is okey. The alternative implementation of this is to rewrite Rewriting |
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 |
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. |
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 |
Hello. Unfortunately the transition name in a petri name should be unique. Anyway, it's simpler to add a new attribute |
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 |
no, with |
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? |
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.
In https://en.wikipedia.org/wiki/Petri_net#Formal_definition_and_basic_terminology:
And a set is a:
|
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. |
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. |
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? |
Right now, no. But I have a bad feeling about it. :/ |
Please elaborate. Im happy to challenge this PR. This PR brings more flexibility. We could make sure to restrict us by forcing the transitions names to be unique whenever we add transition to the |
(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? |
@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:
|
So, what do we do here? |
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 Workflow extends PetriNet {
public function __construct($definition, ...) {
// Validate that the definition is a valid Workflow
}
}
class StateMachine extends PetriNet (Or WorkFlow) {
public function __construct($definition, ...) {
// Validate that the definition is a valid StateMachine
}
} This would enforce the rules, remove the need for |
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. |
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:
|
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 does not 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 ($markingStore instanceof 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 of getTransitions
as Transition[]
, 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
{ | ||
public function __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
EventDispatcherInterface $dispatcher = null, | ||
$name = 'unnamed' | ||
) { | ||
parent::__construct($definition, $markingStore, $dispatcher, $name); | ||
|
||
// If the marking can contain only one place, we should control the definition | ||
if ($markingStore instanceof 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.
@@ -402,12 +402,13 @@ private function registerWorkflowConfiguration(array $workflows, ContainerBuilde | |||
$markingStoreDefinition = new Reference($workflow['marking_store']['service']); | |||
} | |||
|
|||
$workflowDefinition = new DefinitionDecorator('workflow.abstract'); | |||
$type = $workflow['type']; | |||
$workflowDefinition = new DefinitionDecorator($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 use sprintf
for all string concatenation, to be consistent.
@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. |
Is this such PR? |
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. |
Okey. Good. Im fine with either of those. But I would appreciate (of course) if the workflow component was introduced in 3.2. |
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. |
Is there a list of actionables for this PR to be merged already?
Ghehe, that won't happen ;) |
@rufinus new features are never added in patch releases |
@lyrixx I think we're waiting for your final approval here :) |
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 the new branch. Warning: If you want to tests this project, you have to manually 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 ef075134f64a9b31e4d4c7ab7fc14d6c37b172bb
Author: Grégoire Pineau <[email protected]>
Date: Mon Nov 7 18:57:54 2016 +0100
Patch
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php
index 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.php
index 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.xml
index 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 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 /> <!-- marking store -->
+ <argument type="constant">null</argument> <!-- marking store -->
<argument type="service" id="event_dispatcher" on-invalid="ignore" />
<argument /> <!-- name -->
</service>
diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php
index 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.php
index 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,
$name
diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php
index 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 <[email protected]>
@@ -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;
}
} |
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? |
@Nyholm Thanks. I just pushed my patch in your fork. And so my commit just land in this pr ;) 👍 |
I just rebased the PR and squashed all your commits. |
Thank you |
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-boarding bdd3f95 Make the Workflow support State Machines
Thank you all for the feedback! You are awesome! |
@Nyholm is it a wanted effect that no services a created for the state-machines? |
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. |
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 ANY input (froms) place matches the tokens (marking). The default behavior is that ALL input places must match the tokens.