[Workflow] Introducing the workflow component #11882

Merged
merged 2 commits into from Jun 23, 2016

Conversation

Projects
None yet
@lyrixx
Member

lyrixx commented Sep 8, 2014

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? not yet
Fixed tickets n/a
License MIT
Doc PR n/a

TODO:

  • Add tests
  • Add PHP doc
  • Add Symfony fullstack integration (Config, DIC, command to dump the state-machine into graphiz format)

So why another component?

This component take another approach that what you can find on Packagist.

Here, the workflow component is not tied to a specific object like with Finite. It means that the component workflow is stateless and can be a symfony service.

Some code:

#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;

class Foo
{
    public $marking;

    public function __construct($init = 'a')
    {
        $this->marking = $init;
        $this->marking = [$init => 1];
    }
}

$fooDefinition = new Definition(Foo::class);
$fooDefinition->addPlaces([
    'a', 'b', 'c', 'd', 'e', 'f', 'g',
]);

//                                           name  from        to
$fooDefinition->addTransition(new Transition('t1', 'a',        ['b', 'c']));
$fooDefinition->addTransition(new Transition('t2', ['b', 'c'],  'd'));
$fooDefinition->addTransition(new Transition('t3', 'd',         'e'));
$fooDefinition->addTransition(new Transition('t4', 'd',         'f'));
$fooDefinition->addTransition(new Transition('t5', 'e',         'g'));
$fooDefinition->addTransition(new Transition('t6', 'f',         'g'));

$graph = (new GraphvizDumper())->dump($fooDefinition);

$ed = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch(), new \Monolog\Logger('app'));

// $workflow = new Workflow($fooDefinition, new ScalarMarkingStore(), $ed);
$workflow = new Workflow($fooDefinition, new PropertyAccessorMarkingStore(), $ed);

$foo = new Foo(isset($argv[1]) ? $argv[1] : 'a');

$graph = (new GraphvizDumper())->dump($fooDefinition, $workflow->getMarking($foo));

dump([
    'AvailableTransitions' => $workflow->getAvailableTransitions($foo),
    'CurrentMarking' => clone $workflow->getMarking($foo),
    'can validate t1' => $workflow->can($foo, 't1'),
    'can validate t3' => $workflow->can($foo, 't3'),
    'can validate t6' => $workflow->can($foo, 't6'),
    'apply t1' => clone $workflow->apply($foo, 't1'),
    'can validate t2' => $workflow->can($foo, 't2'),
    'apply t2' => clone $workflow->apply($foo, 't2'),
    'can validate t1 bis' => $workflow->can($foo, 't1'),
    'can validate t3 bis' => $workflow->can($foo, 't3'),
    'can validate t6 bis' => $workflow->can($foo, 't6'),
]);

The workflown:

workflow

The output:

array:10 [
  "AvailableTransitions" => array:1 [
    0 => Symfony\Component\Workflow\Transition {#4
      -name: "t1"
      -froms: array:1 [
        0 => "a"
      ]
      -tos: array:2 [
        0 => "b"
        1 => "c"
      ]
    }
  ]
  "CurrentMarking" => Symfony\Component\Workflow\Marking {#19
    -places: array:1 [
      "a" => true
    ]
  }
  "can validate t1" => true
  "can validate t3" => false
  "can validate t6" => false
  "apply t1" => Symfony\Component\Workflow\Marking {#22
    -places: array:2 [
      "b" => true
      "c" => true
    ]
  }
  "apply t2" => Symfony\Component\Workflow\Marking {#47
    -places: array:1 [
      "d" => true
    ]
  }
  "can validate t1 bis" => false
  "can validate t3 bis" => true
  "can validate t6 bis" => false
]

@lyrixx lyrixx changed the title from [WIP][Worflow] Introducing the workflow component to [WIP][Workflow] Introducing the workflow component Sep 8, 2014

@webmozart

This comment has been minimized.

Show comment
Hide comment
@webmozart

webmozart Sep 8, 2014

Contributor

Hi @lyrixx, thank you for your PR! Before I review the code in detail, I'd like to know why you don't propose your changes to yohang/Finite? Their code and docs look useful so far, and I'd rather see an existing project improved than starting completely new efforts.

Contributor

webmozart commented Sep 8, 2014

Hi @lyrixx, thank you for your PR! Before I review the code in detail, I'd like to know why you don't propose your changes to yohang/Finite? Their code and docs look useful so far, and I'd rather see an existing project improved than starting completely new efforts.

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Sep 8, 2014

Member

@webmozart Actually, I did not start this work (as you can see in the commit history). But I think he did not PR finite because the initial design was flawed. So a full rewrite was needed. Then, adding this kind of component to symfony will give more visibility and a community support than in a standalone library (as good as it can be). Finally, as sate on symfony.com Symfony is a set of reusable PHP components and this kind of component deserve much more visibility. I see too much people implementing kind of state machine, instead of using a real one, just because they did not know this problematic exists and there are solutions.

Member

lyrixx commented Sep 8, 2014

@webmozart Actually, I did not start this work (as you can see in the commit history). But I think he did not PR finite because the initial design was flawed. So a full rewrite was needed. Then, adding this kind of component to symfony will give more visibility and a community support than in a standalone library (as good as it can be). Finally, as sate on symfony.com Symfony is a set of reusable PHP components and this kind of component deserve much more visibility. I see too much people implementing kind of state machine, instead of using a real one, just because they did not know this problematic exists and there are solutions.

+ public function getFunctions()
+ {
+ return array(
+ 'can' => new \Twig_Function_Method($this, 'canTransition'),

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

IMO, can is too generic as name. The risk of conflicts is too high here, and it is probably not clear in templates

@stof

stof Sep 8, 2014

Member

IMO, can is too generic as name. The risk of conflicts is too high here, and it is probably not clear in templates

This comment has been minimized.

@florianv

florianv Sep 8, 2014

Contributor

Indeed, isEnabled would be a better name

@florianv

florianv Sep 8, 2014

Contributor

Indeed, isEnabled would be a better name

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

is_enabled is still far too generic. You can have many enabled things which have nothing to do with workflows (users can be enabled in Symfony for instance).

Thus, I don't see how is_enabled would give you the meaning of being allowed to follow this transition

@stof

stof Sep 8, 2014

Member

is_enabled is still far too generic. You can have many enabled things which have nothing to do with workflows (users can be enabled in Symfony for instance).

Thus, I don't see how is_enabled would give you the meaning of being allowed to follow this transition

This comment has been minimized.

@florianv

florianv Sep 8, 2014

Contributor

isEnabled is the exact Petrinet term for saying a transition may fire. Maybe transition_enabled or workflow_transition_enabled would be better to avoid confusion with other enabled things yes

@florianv

florianv Sep 8, 2014

Contributor

isEnabled is the exact Petrinet term for saying a transition may fire. Maybe transition_enabled or workflow_transition_enabled would be better to avoid confusion with other enabled things yes

This comment has been minimized.

@KingCrunch

KingCrunch Sep 8, 2014

Contributor

Because a state machine is a graph the real term is "transition exists" or something like that. Whether or not you can move from one state to another depends on the presence (or absence) of an edge between nodes, but there is no flag "enabled".

@KingCrunch

KingCrunch Sep 8, 2014

Contributor

Because a state machine is a graph the real term is "transition exists" or something like that. Whether or not you can move from one state to another depends on the presence (or absence) of an edge between nodes, but there is no flag "enabled".

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

@KingCrunch this is wrong. Even if the transition exists on the current state, you might not be able to apply it because of guards

@stof

stof Sep 8, 2014

Member

@KingCrunch this is wrong. Even if the transition exists on the current state, you might not be able to apply it because of guards

This comment has been minimized.

@gquemener

gquemener Sep 11, 2014

Contributor

What about throwing exception when transition is not applyable?

@gquemener

gquemener Sep 11, 2014

Contributor

What about throwing exception when transition is not applyable?

This comment has been minimized.

@stof

stof Sep 12, 2014

Member

the goal of this function is to be able to knwo whether you can perform this transition. the use case is knowing whether you should display a button triggering it in the interface. Exceptions have nothing to do here. This function does not attempt to apply the transition, but only to know whether it is possible

@stof

stof Sep 12, 2014

Member

the goal of this function is to be able to knwo whether you can perform this transition. the use case is knowing whether you should display a button triggering it in the interface. Exceptions have nothing to do here. This function does not attempt to apply the transition, but only to know whether it is possible

This comment has been minimized.

@gquemener

gquemener Sep 12, 2014

Contributor

Didn't look at the file the function was...

@gquemener

gquemener Sep 12, 2014

Contributor

Didn't look at the file the function was...

+ {
+ foreach (array('graph', 'node', 'edge') as $key) {
+ if (isset($options[$key])) {
+ $this->options[$key] = array_merge($this->options[$key], $options[$key]);

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

this is a bad idea. It means that the options passed to a first call to dump will have an impact on the following calls to dump

@stof

stof Sep 8, 2014

Member

this is a bad idea. It means that the options passed to a first call to dump will have an impact on the following calls to dump

+ $nodes = array();
+ foreach ($definition->getStates() as $state) {
+ $nodes[$state] = array(
+ 'attributes' => array_merge($this->options['node'], array('style' => $state == $definition->getInitialState() ? 'filled' : 'solid'))

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

should be a strict comparison

@stof

stof Sep 8, 2014

Member

should be a strict comparison

+ }
+ }
+
+ $this->nodes = $this->findNodes($definition);

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

instead of storing this in a property (which is not reset at the end of the dumping, thus leaking memory if you keep a reference to the dumper itself), I would rather use a local variable and pass it as argument to addNodes

@stof

stof Sep 8, 2014

Member

instead of storing this in a property (which is not reset at the end of the dumping, thus leaking memory if you keep a reference to the dumper itself), I would rather use a local variable and pass it as argument to addNodes

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

and the same is true for edges

@stof

stof Sep 8, 2014

Member

and the same is true for edges

+ {
+ $code = array();
+ foreach ($options as $k => $v) {
+ $code[] = sprintf('%s="%s"', $k, $v);

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

you need to escape the quotes in the value

@stof

stof Sep 8, 2014

Member

you need to escape the quotes in the value

+ {
+ $code = array();
+ foreach ($attributes as $k => $v) {
+ $code[] = sprintf('%s="%s"', $k, $v);

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

you need to escape the quotes in the value

@stof

stof Sep 8, 2014

Member

you need to escape the quotes in the value

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

the key as well btw. See https://github.com/alexandresalome/graphviz/blob/master/src/Alom/Graphviz/BaseInstruction.php for the escaping rules for Graphviz

@cordoval

This comment has been minimized.

Show comment
Hide comment
@cordoval

cordoval Sep 8, 2014

Contributor

@lyrixx i agree that it needs visibility. However, I would like to know the thoughts of @winzou https://github.com/winzou/state-machine (more official imo than finite lib) since his has reached a good level. Sylius already uses this successfully and several other projects (though still a simple SM). Going to a petri net implementation (this other is not but related http://github.com/vespolina/workflow) cc/ @iampersistent could be very good, but the state of this component is not that right now and there seems to be a big gap. reference workflowpatterns.com. Other than that 👍 to learn of a good colored petri net attempt!

Contributor

cordoval commented Sep 8, 2014

@lyrixx i agree that it needs visibility. However, I would like to know the thoughts of @winzou https://github.com/winzou/state-machine (more official imo than finite lib) since his has reached a good level. Sylius already uses this successfully and several other projects (though still a simple SM). Going to a petri net implementation (this other is not but related http://github.com/vespolina/workflow) cc/ @iampersistent could be very good, but the state of this component is not that right now and there seems to be a big gap. reference workflowpatterns.com. Other than that 👍 to learn of a good colored petri net attempt!

+ */
+ private function dotize($id)
+ {
+ return strtolower(preg_replace('/[^\w]/i', '_', $id));

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

you don't need to transform the states and transformation names if you apply escaping on the node names properly instead

@stof

stof Sep 8, 2014

Member

you don't need to transform the states and transformation names if you apply escaping on the node names properly instead

+ return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
+ }
+
+ public function hastAttribute($key)

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

typo

@stof

stof Sep 8, 2014

Member

typo

+ */
+class GuardEvent extends Event
+{
+ private $allowed = null;

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

shouldn't it be initialized to a boolean instead ?

and otherwise, = null should be removed

@stof

stof Sep 8, 2014

Member

shouldn't it be initialized to a boolean instead ?

and otherwise, = null should be removed

This comment has been minimized.

@eddiejaoude

eddiejaoude Dec 30, 2014

Contributor

Docbloc would be helpful to clarify type.

If bool, property should be renamed to $isAllowed

@eddiejaoude

eddiejaoude Dec 30, 2014

Contributor

Docbloc would be helpful to clarify type.

If bool, property should be renamed to $isAllowed

+ public function onEnter(Event $event)
+ {
+// FIXME: object "identity", timestamp, who, ...
+error_log('entering "'.$event->getState().'" generic for object of class '.get_class($event->getObject()));

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

Please use a PSR-3 logger rather than error_log

@stof

stof Sep 8, 2014

Member

Please use a PSR-3 logger rather than error_log

+ public static function getSubscribedEvents()
+ {
+ return array(
+// FIXME: add a way to listen to workflow.XXX.*

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

are you looking at integrating https://github.com/jmikola/WildcardEventDispatcher in core ?

@stof

stof Sep 8, 2014

Member

are you looking at integrating https://github.com/jmikola/WildcardEventDispatcher in core ?

Symfony/Component/Workflow/Workflow.php
+
+ public function getDefinition()
+ {
+ return $this->definition;

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

does a workflow really need to be able to return its mutable definition here, while changing it will not change the workflow actually as the Workflow is frozen with the definition passed to the constructor ?
If no, I would suggest removing the getter and removing the property storing it

@stof

stof Sep 8, 2014

Member

does a workflow really need to be able to return its mutable definition here, while changing it will not change the workflow actually as the Workflow is frozen with the definition passed to the constructor ?
If no, I would suggest removing the getter and removing the property storing it

Symfony/Component/Workflow/Workflow.php
+ }
+ }
+
+ public function supports($class)

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

this is misleading. the usage of instanceof means you pass an object, not a class

@stof

stof Sep 8, 2014

Member

this is misleading. the usage of instanceof means you pass an object, not a class

This comment has been minimized.

@eddiejaoude

eddiejaoude Dec 30, 2014

Contributor

If an object is passed can it be type hinted to an interface?

@eddiejaoude

eddiejaoude Dec 30, 2014

Contributor

If an object is passed can it be type hinted to an interface?

Symfony/Component/Workflow/composer.json
+ "name": "symfony/workflow",
+ "type": "library",
+ "description": "Symfony Workflow Component",
+ "keywords": [],

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

you should add some keywords

@stof

stof Sep 8, 2014

Member

you should add some keywords

Symfony/Component/Workflow/composer.json
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.5-dev"

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

2.6 actually

@stof

stof Sep 8, 2014

Member

2.6 actually

Symfony/Component/Workflow/Workflow.php
+ public function can($object, $transition)
+ {
+ if (!isset($this->transitions[$transition])) {
+ throw new \LogicException(sprintf('Transition "%s" does not exist for workflow "%s".', $transition, $this->name));

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

I think using dedicated exceptions would be better: InvalidTransitionException, InvalidStateException, etc... (and all implementing a common marker interface for people wanting to catch them)

@stof

stof Sep 8, 2014

Member

I think using dedicated exceptions would be better: InvalidTransitionException, InvalidStateException, etc... (and all implementing a common marker interface for people wanting to catch them)

Symfony/Component/Workflow/Workflow.php
+
+ public function getAvailableTransitions($object)
+ {
+ if (!isset($this->stateTransitions[$this->getState($object)])) {

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

you should use a local variable to store the state instead of calling getState() twice (this is not a simple getter)

@stof

stof Sep 8, 2014

Member

you should use a local variable to store the state instead of calling getState() twice (this is not a simple getter)

Symfony/Component/Workflow/Workflow.php
+ $this->states = $definition->getStates();
+ $this->class = $definition->getClass();
+ $this->initialState = $definition->getInitialState();
+ foreach ($definition->getTransitions() as $name => $transition) {

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

$name here is overwriting the $name argument. While this does not introduce any bug here because the argument is used earlier only, it would be better to avoid this. It would avoid weird bugs introduced in the future and also keep things more readable (readers who are not very cautious could think than $name is the argument 3 lines below)

@stof

stof Sep 8, 2014

Member

$name here is overwriting the $name argument. While this does not introduce any bug here because the argument is used earlier only, it would be better to avoid this. It would avoid weird bugs introduced in the future and also keep things more readable (readers who are not very cautious could think than $name is the argument 3 lines below)

Symfony/Component/Workflow/Workflow.php
+ $this->property = $property;
+ }
+
+ public function setPropertyAccessor(PropertyAccessor $propertyAccessor)

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

the interface should be used instead

@stof

stof Sep 8, 2014

Member

the interface should be used instead

Symfony/Component/Workflow/Registry.php
+ public function get($object)
+ {
+ foreach ($this->workflows as $workflow) {
+ if ($workflow->supports($object)) {

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

This implementation makes it impossible to manage 2 independant workflows for the same object in different properties, because both will support the object. there should be a way to get a workflow based on its name too rather than based on its support of an object

@stof

stof Sep 8, 2014

Member

This implementation makes it impossible to manage 2 independant workflows for the same object in different properties, because both will support the object. there should be a way to get a workflow based on its name too rather than based on its support of an object

@stof

This comment has been minimized.

Show comment
Hide comment
@stof

stof Sep 8, 2014

Member

@cordoval https://github.com/winzou/state-machine suffers from the same architecture flaw than Finite: the state machine is built around the object, so you cannot make it a service. If you have several objects which need to follow the workflow, you need to instantiate a separate state machine for each of them.

Note that when I'm saying architecture flaw, it does not mean it is entirely bad. But it does not fit in a DI context. It depends of the architecture of your projects (putting the state machine in the object itself can look the right way when using active record with propel for instance: http://williamdurand.fr/StateMachineBehavior/)

Member

stof commented Sep 8, 2014

@cordoval https://github.com/winzou/state-machine suffers from the same architecture flaw than Finite: the state machine is built around the object, so you cannot make it a service. If you have several objects which need to follow the workflow, you need to instantiate a separate state machine for each of them.

Note that when I'm saying architecture flaw, it does not mean it is entirely bad. But it does not fit in a DI context. It depends of the architecture of your projects (putting the state machine in the object itself can look the right way when using active record with propel for instance: http://williamdurand.fr/StateMachineBehavior/)

@cordoval

This comment has been minimized.

Show comment
Hide comment
@cordoval

cordoval Sep 8, 2014

Contributor

i see, so the idea is to freeze things into the container as well. I am getting the gist of it now. Agree 👍

Contributor

cordoval commented Sep 8, 2014

i see, so the idea is to freeze things into the container as well. I am getting the gist of it now. Agree 👍

@peterrehm

This comment has been minimized.

Show comment
Hide comment
@peterrehm

peterrehm Sep 8, 2014

Contributor

@lyrixx In the sample code in the PR you have a typo with $foor where you meant $foo. And also the variable $order should be renamed to $foo or $foo/$foor should be renamed to $order.

Contributor

peterrehm commented Sep 8, 2014

@lyrixx In the sample code in the PR you have a typo with $foor where you meant $foo. And also the variable $order should be renamed to $foo or $foo/$foor should be renamed to $order.

+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class GuardEvent extends Event

This comment has been minimized.

@florianv

florianv Sep 8, 2014

Contributor

Normally guards are attached to the transitions and part of the workflow definition (which could be persisted)

@florianv

florianv Sep 8, 2014

Contributor

Normally guards are attached to the transitions and part of the workflow definition (which could be persisted)

+ $this->initialState = $name;
+ }
+
+ public function addState($name)

This comment has been minimized.

@florianv

florianv Sep 8, 2014

Contributor

The state is a node and IMO should be an object like the transitions

@florianv

florianv Sep 8, 2014

Contributor

The state is a node and IMO should be an object like the transitions

This comment has been minimized.

@stof

stof Sep 8, 2014

Member

I don't think an objct wrapping the string is really necessary. It would only add overhead

@stof

stof Sep 8, 2014

Member

I don't think an objct wrapping the string is really necessary. It would only add overhead

This comment has been minimized.

@florianv

florianv Sep 8, 2014

Contributor

The definition could be persisted to a database (for example an article publication workflow for a cms, or an issue state workflow for a project management software). I think making it a separate entity makes more sense as extra data could be associated to the state.

Also I would identify a state by an id instead of a name. Adding the name would require using an object.

@florianv

florianv Sep 8, 2014

Contributor

The definition could be persisted to a database (for example an article publication workflow for a cms, or an issue state workflow for a project management software). I think making it a separate entity makes more sense as extra data could be associated to the state.

Also I would identify a state by an id instead of a name. Adding the name would require using an object.

@trompette

This comment has been minimized.

Show comment
Hide comment
@trompette

trompette Sep 8, 2014

Contributor

I contributed a small component for a project we did at Alter Way: https://github.com/alterway/component-workflow. I'm not sure if it is possible, but some ideas could be shared.

Contributor

trompette commented Sep 8, 2014

I contributed a small component for a project we did at Alter Way: https://github.com/alterway/component-workflow. I'm not sure if it is possible, but some ideas could be shared.

+
+use Symfony\Component\Workflow\Registry;
+
+class WorkflowExtension extends \Twig_Extension

This comment has been minimized.

@jderusse

jderusse Sep 8, 2014

Contributor

Missing @author

@jderusse

jderusse Sep 8, 2014

Contributor

Missing @author

@florianv

This comment has been minimized.

Show comment
Hide comment
@florianv

florianv Sep 8, 2014

Contributor

For information, I created a basic Petrinet library some time ago https://github.com/florianv/petrinet.

Contributor

florianv commented Sep 8, 2014

For information, I created a basic Petrinet library some time ago https://github.com/florianv/petrinet.

@cordoval

This comment has been minimized.

Show comment
Hide comment
@cordoval

cordoval Sep 8, 2014

Contributor

@florianv it is very good! very! 👍 thanks!

Contributor

cordoval commented Sep 8, 2014

@florianv it is very good! very! 👍 thanks!

@shoomyth

This comment has been minimized.

Show comment
Hide comment
@shoomyth

shoomyth Sep 8, 2014

Could anyone provide some real-world use-cases where this component can be used?

shoomyth commented Sep 8, 2014

Could anyone provide some real-world use-cases where this component can be used?

@stof

This comment has been minimized.

Show comment
Hide comment
@stof

stof Sep 8, 2014

Member

@lyrixx regarding the Graphviz dumping, I think the current implementation is wrong according to what the code implements.
Your dumped graph is using edges to represent transitions. This would work fine for a state machine. However, the petri net represented by your Workflow is not a state machine: a transition can have several input places and several output places (getFroms and getTos are returning arrays). The consequence is that you are current duplicating transitions in the graph. Switching to a representation of a petri net where both states (places in term of petri net) and transitions are nodes (represented differently) might more accurate

Member

stof commented Sep 8, 2014

@lyrixx regarding the Graphviz dumping, I think the current implementation is wrong according to what the code implements.
Your dumped graph is using edges to represent transitions. This would work fine for a state machine. However, the petri net represented by your Workflow is not a state machine: a transition can have several input places and several output places (getFroms and getTos are returning arrays). The consequence is that you are current duplicating transitions in the graph. Switching to a representation of a petri net where both states (places in term of petri net) and transitions are nodes (represented differently) might more accurate

@mickaelandrieu

This comment has been minimized.

Show comment
Hide comment
@mickaelandrieu

mickaelandrieu Sep 8, 2014

Contributor

Assured me, this contribution is subject to a vote to be merged in Symfony? I feel that it is already acquired and it seems a bit fast: is there a real interest in making an official component rather than a library with his Symfony bundle ?

State machine is ** not ** a common problem of web application imo.

Contributor

mickaelandrieu commented Sep 8, 2014

Assured me, this contribution is subject to a vote to be merged in Symfony? I feel that it is already acquired and it seems a bit fast: is there a real interest in making an official component rather than a library with his Symfony bundle ?

State machine is ** not ** a common problem of web application imo.

+
+ public function setAllowed($allowed)
+ {
+ $this->allowed = (Boolean) $allowed;

This comment has been minimized.

@cordoval

cordoval Sep 8, 2014

Contributor

this also here should just be bool

@cordoval

cordoval Sep 8, 2014

Contributor

this also here should just be bool

@florianv

This comment has been minimized.

Show comment
Hide comment
@florianv

florianv Sep 8, 2014

Contributor

@cordoval Thanks! Colored Petrinets are much more powerful but quite complex specially when there are arc expressions, conditions and guards.

Contributor

florianv commented Sep 8, 2014

@cordoval Thanks! Colored Petrinets are much more powerful but quite complex specially when there are arc expressions, conditions and guards.

@Nicofuma

This comment has been minimized.

Show comment
Hide comment
@Nicofuma

Nicofuma Sep 9, 2014

Contributor

@mickaelandrieu simple state machine is a very common problem for any application (especially when it's multi user) even it's not always seen. Sometimes it could be a little overkill to use a real state machine to do the work (e.g. with some small and simple workflows) but I think that it depends of the overhead introduced and anyway it's much cleaner and maintainable to use a real state machine if it's possible to describe/look at the workflow without having to get deep into the code.

Contributor

Nicofuma commented Sep 9, 2014

@mickaelandrieu simple state machine is a very common problem for any application (especially when it's multi user) even it's not always seen. Sometimes it could be a little overkill to use a real state machine to do the work (e.g. with some small and simple workflows) but I think that it depends of the overhead introduced and anyway it's much cleaner and maintainable to use a real state machine if it's possible to describe/look at the workflow without having to get deep into the code.

@gnugat

This comment has been minimized.

Show comment
Hide comment
@gnugat

gnugat Sep 9, 2014

Contributor

A stateless state machine service seems great!
The name sounds a bit non informative though: why wouldn't it be StateMachine or PetriNet?

Contributor

gnugat commented Sep 9, 2014

A stateless state machine service seems great!
The name sounds a bit non informative though: why wouldn't it be StateMachine or PetriNet?

@stof

This comment has been minimized.

Show comment
Hide comment
@stof

stof Jun 22, 2016

Member

@fabpot could you enable the LongArraySyntaxFixer contrib fixer on fabbot.io for the Symfony repo, so that enforcing consistent usage of the long array syntax is done by the tool rather than by the reviewers ?

Member

stof commented Jun 22, 2016

@fabpot could you enable the LongArraySyntaxFixer contrib fixer on fabbot.io for the Symfony repo, so that enforcing consistent usage of the long array syntax is done by the tool rather than by the reviewers ?

+{
+ public function testItWorks()
+ {
+ $transitions = [

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

+ new Transition('t2', 'a', 'b'),
+ ];
+
+ $definition = new Definition(['a', 'b'], $transitions);

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

+
+ $workflow->apply($object, 't1');
+
+ $expected = [

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

This comment has been minimized.

@stof

stof Jun 22, 2016

Member

@HeahDude please don't comment for this everywhere. fabbot.io will handle it on next push to the PR

@stof

stof Jun 22, 2016

Member

@HeahDude please don't comment for this everywhere. fabbot.io will handle it on next push to the PR

+
+ $markingStore->setMarking($subject, $marking);
+
+ $this->assertSame(['first_place' => 1], $subject->myMarks);

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

+
+ $this->assertTrue($marking->has('a'));
+ $this->assertFalse($marking->has('b'));
+ $this->assertSame(['a' => 1], $marking->getPlaces());

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

+
+ $this->assertTrue($marking->has('a'));
+ $this->assertTrue($marking->has('b'));
+ $this->assertSame(['a' => 1, 'b' => 1], $marking->getPlaces());

This comment has been minimized.

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@HeahDude

HeahDude Jun 22, 2016

Member

array(...)

@fabpot

This comment has been minimized.

Show comment
Hide comment
@fabpot

fabpot Jun 22, 2016

Member

It has been enabled yesterday

Member

fabpot commented Jun 22, 2016

It has been enabled yesterday

@fabpot

This comment has been minimized.

Show comment
Hide comment
@fabpot

fabpot Jun 22, 2016

Member

ah, no, I enabled it on Symfony, but I forgot to deploy the same on fabbot.io. Fixed now

Member

fabpot commented Jun 22, 2016

ah, no, I enabled it on Symfony, but I forgot to deploy the same on fabbot.io. Fixed now

@stof

This comment has been minimized.

Show comment
Hide comment
@stof

stof Jun 22, 2016

Member

@fabpot can you re-trigger the inspection for this PR as it contains many usages of the short array syntax ?

Member

stof commented Jun 22, 2016

@fabpot can you re-trigger the inspection for this PR as it contains many usages of the short array syntax ?

@fabpot

This comment has been minimized.

Show comment
Hide comment
@fabpot

fabpot Jun 22, 2016

Member

Done

Member

fabpot commented Jun 22, 2016

Done

+{
+ public function __construct(LoggerInterface $logger)
+ {
+ $this->logger = $logger;

This comment has been minimized.

@jderusse

jderusse Jun 22, 2016

Contributor

variable is not declared

@jderusse

jderusse Jun 22, 2016

Contributor

variable is not declared

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Jun 22, 2016

Member

I think I have fixed all reported issues.

Member

lyrixx commented Jun 22, 2016

I think I have fixed all reported issues.

@fabpot

This comment has been minimized.

Show comment
Hide comment
@fabpot

fabpot Jun 23, 2016

Member

Thank you @lyrixx.

Member

fabpot commented Jun 23, 2016

Thank you @lyrixx.

@fabpot fabpot merged commit 078e27f into symfony:master Jun 23, 2016

0 of 3 checks passed

continuous-integration/appveyor/pr Waiting for AppVeyor build to complete
Details
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
fabbot.io Doing some proofreading and checking your coding style.
Details

fabpot added a commit that referenced this pull request Jun 23, 2016

feature #11882 [Workflow] Introducing the workflow component (fabpot,…
… lyrixx)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Workflow] Introducing the workflow component

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | not yet
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | n/a

TODO:

* [x] Add tests
* [x] Add PHP doc
* [x] Add Symfony fullstack integration (Config, DIC, command to dump the state-machine into graphiz format)

So why another component?

This component take another approach that what you can find on [Packagist](https://packagist.org/search/?q=state%20machine).

Here, the workflow component is not tied to a specific object like with [Finite](https://github.com/yohang/Finite). It means that the component workflow is stateless and can be a symfony service.

Some code:

```php
#!/usr/bin/env php
<?php

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Dumper\GraphvizDumper;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\MarkingStore\PropertyAccessorMarkingStore;
use Symfony\Component\Workflow\MarkingStore\ScalarMarkingStore;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;

class Foo
{
    public $marking;

    public function __construct($init = 'a')
    {
        $this->marking = $init;
        $this->marking = [$init => true];
    }
}

$fooDefinition = new Definition(Foo::class);
$fooDefinition->addPlaces([
    'a', 'b', 'c', 'd', 'e', 'f', 'g',
]);

//                                           name  from        to
$fooDefinition->addTransition(new Transition('t1', 'a',        ['b', 'c']));
$fooDefinition->addTransition(new Transition('t2', ['b', 'c'],  'd'));
$fooDefinition->addTransition(new Transition('t3', 'd',         'e'));
$fooDefinition->addTransition(new Transition('t4', 'd',         'f'));
$fooDefinition->addTransition(new Transition('t5', 'e',         'g'));
$fooDefinition->addTransition(new Transition('t6', 'f',         'g'));

$graph = (new GraphvizDumper())->dump($fooDefinition);

$ed = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch(), new \Monolog\Logger('app'));

// $workflow = new Workflow($fooDefinition, new ScalarMarkingStore(), $ed);
$workflow = new Workflow($fooDefinition, new PropertyAccessorMarkingStore(), $ed);

$foo = new Foo(isset($argv[1]) ? $argv[1] : 'a');

$graph = (new GraphvizDumper())->dump($fooDefinition, $workflow->getMarking($foo));

dump([
    'AvailableTransitions' => $workflow->getAvailableTransitions($foo),
    'CurrentMarking' => clone $workflow->getMarking($foo),
    'can validate t1' => $workflow->can($foo, 't1'),
    'can validate t3' => $workflow->can($foo, 't3'),
    'can validate t6' => $workflow->can($foo, 't6'),
    'apply t1' => clone $workflow->apply($foo, 't1'),
    'can validate t2' => $workflow->can($foo, 't2'),
    'apply t2' => clone $workflow->apply($foo, 't2'),
    'can validate t1 bis' => $workflow->can($foo, 't1'),
    'can validate t3 bis' => $workflow->can($foo, 't3'),
    'can validate t6 bis' => $workflow->can($foo, 't6'),
]);

```

The workflown:

![workflow](https://cloud.githubusercontent.com/assets/408368/14183999/4a43483c-f773-11e5-9c8b-7f157e0cb75f.png)

The output:

```
array:10 [
  "AvailableTransitions" => array:1 [
    0 => Symfony\Component\Workflow\Transition {#4
      -name: "t1"
      -froms: array:1 [
        0 => "a"
      ]
      -tos: array:2 [
        0 => "b"
        1 => "c"
      ]
    }
  ]
  "CurrentMarking" => Symfony\Component\Workflow\Marking {#19
    -places: array:1 [
      "a" => true
    ]
  }
  "can validate t1" => true
  "can validate t3" => false
  "can validate t6" => false
  "apply t1" => Symfony\Component\Workflow\Marking {#22
    -places: array:2 [
      "b" => true
      "c" => true
    ]
  }
  "apply t2" => Symfony\Component\Workflow\Marking {#47
    -places: array:1 [
      "d" => true
    ]
  }
  "can validate t1 bis" => false
  "can validate t3 bis" => true
  "can validate t6 bis" => false
]
```

Commits
-------

078e27f [Workflow] Added initial set of files
17d59a7 added the first more-or-less working version of the Workflow component

@lyrixx lyrixx deleted the lyrixx:component-workflow branch Jun 23, 2016

@bendavies bendavies referenced this pull request in Sylius/Sylius Jun 23, 2016

Closed

[RFC] Symfony Workflow #5335

@robinduval

This comment has been minimized.

Show comment
Hide comment
@robinduval

robinduval Jun 23, 2016

Contributor

Happy merge @lyrixx

Contributor

robinduval commented Jun 23, 2016

Happy merge @lyrixx

@lyrixx lyrixx referenced this pull request in symfony/symfony-docs Jun 23, 2016

Closed

[Workflow] Add documentation for the Workflow Component #6677

@parweb

This comment has been minimized.

Show comment
Hide comment
@parweb

parweb Jul 3, 2016

@lyrixx , you did a great job.

I implemented the component in my project, but I don't understand something.

The "announce" event ?

Can you explain a bit what's the propose of this ?
Or a use case ?

guard ----> leave ----> transition ----> enter
those events i see clearly what can i do at what moment but announce it's not clear.
Because it's about the next state.

parweb commented Jul 3, 2016

@lyrixx , you did a great job.

I implemented the component in my project, but I don't understand something.

The "announce" event ?

Can you explain a bit what's the propose of this ?
Or a use case ?

guard ----> leave ----> transition ----> enter
those events i see clearly what can i do at what moment but announce it's not clear.
Because it's about the next state.

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Jul 3, 2016

Member

Hello @parweb

The announce event is here to fit some needs asked in this comment. If it's still not clear for you, don't hesitate to ask ;)

Member

lyrixx commented Jul 3, 2016

Hello @parweb

The announce event is here to fit some needs asked in this comment. If it's still not clear for you, don't hesitate to ask ;)

@parweb

This comment has been minimized.

Show comment
Hide comment
@parweb

parweb Jul 3, 2016

@lyrixx i think i understand the purpose better now.

When a new transition is available an event is fire ?

parweb commented Jul 3, 2016

@lyrixx i think i understand the purpose better now.

When a new transition is available an event is fire ?

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Jul 4, 2016

Member

Yes.

Member

lyrixx commented Jul 4, 2016

Yes.

@tacman

This comment has been minimized.

Show comment
Hide comment
@tacman

tacman Jul 19, 2016

Is there any other documentation besides the first post in this issue? Is that documentation still current? I've been using the demo app code (https://github.com/lyrixx/SFLive-Paris2016-Workflow), but now need to dynamically create a workflow instead of coding it in the config file. Thanks.

tacman commented Jul 19, 2016

Is there any other documentation besides the first post in this issue? Is that documentation still current? I've been using the demo app code (https://github.com/lyrixx/SFLive-Paris2016-Workflow), but now need to dynamically create a workflow instead of coding it in the config file. Thanks.

@hacfi

This comment has been minimized.

Show comment
Hide comment
@hacfi

hacfi Jul 20, 2016

Contributor

@tacman Afaik not yet! The goal is to have the docs ready when 3.2 will be released which according to http://symfony.com/doc/current/contributing/community/releases.html is November 2016.

Contributor

hacfi commented Jul 20, 2016

@tacman Afaik not yet! The goal is to have the docs ready when 3.2 will be released which according to http://symfony.com/doc/current/contributing/community/releases.html is November 2016.

@cravler

This comment has been minimized.

Show comment
Hide comment
@cravler

cravler Jul 25, 2016

Is this doable?

workfolw

cravler commented Jul 25, 2016

Is this doable?

workfolw

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Jul 25, 2016

Member

yes

Member

lyrixx commented Jul 25, 2016

yes

@cravler

This comment has been minimized.

Show comment
Hide comment

cravler commented Jul 25, 2016

Example?

@lyrixx

This comment has been minimized.

Show comment
Hide comment
@lyrixx

lyrixx Jul 25, 2016

Member

@cravler Could you, please, be polite and make real sentences. I'm not a bot.

places: [a, b, c, d]
transitions:
  t1: { from: a , to: [b, c]}
  t2: { from: [b, c] , to: d}
Member

lyrixx commented Jul 25, 2016

@cravler Could you, please, be polite and make real sentences. I'm not a bot.

places: [a, b, c, d]
transitions:
  t1: { from: a , to: [b, c]}
  t2: { from: [b, c] , to: d}
@cravler

This comment has been minimized.

Show comment
Hide comment
@cravler

cravler Jul 25, 2016

Step 1
workfolw1
Step 2
workflow2
OR
workflow2a
Step 3
workflow3
Step 4
workflow4

cravler commented Jul 25, 2016

Step 1
workfolw1
Step 2
workflow2
OR
workflow2a
Step 3
workflow3
Step 4
workflow4

@mdwheele

This comment has been minimized.

Show comment
Hide comment
@mdwheele

mdwheele Jul 25, 2016

@cravler You should really add more to your comments so intent is clear. I have extra time, so I'll guess.

If your intent is that the order in which paid and sent are marked matters, then you should be using some form of sequential processing. In this case, when t1 fires, both paid and sent are immediately marked. In fact, there is no "Step 2" and "Step 3". "Step 1" is accurate. The next marking would be something similar to your "Step 3" except packed should not be marked (which is what I'm assuming green means). The next marking after t2 fires would be that delivered be marked with no other places marked.

You probably want something like:

(pseudo-markup; unsure if this is valid configuration for workflow component)

places: [packed, paid, sent, delivered]
transitions:
  t1: { from: packed, to: paid}
  t2: { from: paid, to: sent}
  t3: { from: sent, to: delivered}

Go check out http://www.workflowpatterns.com. It is a valuable resource for learning how these data structures work and gives you some hints on what patterns exist and what they're good for. Most should be able to be implemented using this component from what I have seen.

mdwheele commented Jul 25, 2016

@cravler You should really add more to your comments so intent is clear. I have extra time, so I'll guess.

If your intent is that the order in which paid and sent are marked matters, then you should be using some form of sequential processing. In this case, when t1 fires, both paid and sent are immediately marked. In fact, there is no "Step 2" and "Step 3". "Step 1" is accurate. The next marking would be something similar to your "Step 3" except packed should not be marked (which is what I'm assuming green means). The next marking after t2 fires would be that delivered be marked with no other places marked.

You probably want something like:

(pseudo-markup; unsure if this is valid configuration for workflow component)

places: [packed, paid, sent, delivered]
transitions:
  t1: { from: packed, to: paid}
  t2: { from: paid, to: sent}
  t3: { from: sent, to: delivered}

Go check out http://www.workflowpatterns.com. It is a valuable resource for learning how these data structures work and gives you some hints on what patterns exist and what they're good for. Most should be able to be implemented using this component from what I have seen.

@mdwheele

This comment has been minimized.

Show comment
Hide comment
@mdwheele

mdwheele Jul 25, 2016

When a new transition is available an event is fire ?

@parweb That's exactly correct! The immediate use-case I had for this at the time was integration of a workflow with a work item delegator as part of a workflow management system. In the language of Workflow Nets, "places" are called "conditions" and "transitions" are called "tasks". Some tasks are "user / input triggered" meaning that they do not fire immediately upon becoming enabled. An enabled task becomes a "work item" available for work applied by some resource. This is why it is handy to know when a transition becomes enabled; so that a collaborator can delegate responsibility for a work item to one or more candidate resources. Once the work item is completed (outside the scope of this component) then the task (transition) will be fired and the Petri net goes on functioning as per usual.

When a new transition is available an event is fire ?

@parweb That's exactly correct! The immediate use-case I had for this at the time was integration of a workflow with a work item delegator as part of a workflow management system. In the language of Workflow Nets, "places" are called "conditions" and "transitions" are called "tasks". Some tasks are "user / input triggered" meaning that they do not fire immediately upon becoming enabled. An enabled task becomes a "work item" available for work applied by some resource. This is why it is handy to know when a transition becomes enabled; so that a collaborator can delegate responsibility for a work item to one or more candidate resources. Once the work item is completed (outside the scope of this component) then the task (transition) will be fired and the Petri net goes on functioning as per usual.

@cravler

This comment has been minimized.

Show comment
Hide comment

cravler commented Jul 25, 2016

I asked about this: Interleaved Parallel Routing

@mdwheele

This comment has been minimized.

Show comment
Hide comment
@mdwheele

mdwheele Jul 25, 2016

I asked about this: Interleaved Parallel Routing

I believe you could implement Interleaved Parallel Routing (implementation guide) with this component. You have guards / guard events to be able to hook into the execution of the petri net. However, you'll need to implement something to carry the state required to implement the rest, I imagine.

Keep in mind, this package implements a Petri net first and dips its toes into the "pool of Workflow Nets". That said, it does not carry many of the common workflow net building blocks you'd expect in a WFMS offering (nor should it, in my opinion).

Many workflow-net constructs are able to be projected to a lower-level PT-net form, but will require a bit of orchestration around primitives. The linked document above shows a PT-net implementation of a net similar to the graphic you linked. It is clearly not as graceful, but the graphic linked was also an abstraction used for demonstrative purposes.

mdwheele commented Jul 25, 2016

I asked about this: Interleaved Parallel Routing

I believe you could implement Interleaved Parallel Routing (implementation guide) with this component. You have guards / guard events to be able to hook into the execution of the petri net. However, you'll need to implement something to carry the state required to implement the rest, I imagine.

Keep in mind, this package implements a Petri net first and dips its toes into the "pool of Workflow Nets". That said, it does not carry many of the common workflow net building blocks you'd expect in a WFMS offering (nor should it, in my opinion).

Many workflow-net constructs are able to be projected to a lower-level PT-net form, but will require a bit of orchestration around primitives. The linked document above shows a PT-net implementation of a net similar to the graphic you linked. It is clearly not as graceful, but the graphic linked was also an abstraction used for demonstrative purposes.

+
+ 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);

This comment has been minimized.

@lutangar

lutangar Aug 3, 2016

@lyrixx @mdwheele I might be wrong, but I was expecting the dispatched event to hold the announced transition instead of the initial transition.
From where the availabilty of the transition originated from, might not be what's matter the most here.
After all, the use case was to announce which transition are now available.

@lutangar

lutangar Aug 3, 2016

@lyrixx @mdwheele I might be wrong, but I was expecting the dispatched event to hold the announced transition instead of the initial transition.
From where the availabilty of the transition originated from, might not be what's matter the most here.
After all, the use case was to announce which transition are now available.

This comment has been minimized.

@lyrixx

lyrixx Aug 4, 2016

Member

Hello @lutangar ;

I'm not sure to understand what you want. Anyway, If you want a new feature, or to change an existing one, please open a new issue, it will be easier to discuss about it there. And feel free to ping me ;)

@lyrixx

lyrixx Aug 4, 2016

Member

Hello @lutangar ;

I'm not sure to understand what you want. Anyway, If you want a new feature, or to change an existing one, please open a new issue, it will be easier to discuss about it there. And feel free to ping me ;)

@javiereguiluz

This comment has been minimized.

Show comment
Hide comment
@javiereguiluz

javiereguiluz Aug 4, 2016

Member

I'm locking this pull request because it's generating too many messages:

  • If you want to report an error or ask for a new feature, please create a new issue.
  • If you are looking for documentation, see Workflow Component Documentation.

Thank you all for this long discussion!

Member

javiereguiluz commented Aug 4, 2016

I'm locking this pull request because it's generating too many messages:

  • If you want to report an error or ask for a new feature, please create a new issue.
  • If you are looking for documentation, see Workflow Component Documentation.

Thank you all for this long discussion!

@symfony symfony locked and limited conversation to collaborators Aug 4, 2016

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.