diff --git a/_images/components/workflow/blogpost.png b/_images/components/workflow/blogpost.png new file mode 100644 index 00000000000..38e29250eb1 Binary files /dev/null and b/_images/components/workflow/blogpost.png differ diff --git a/_images/components/workflow/job_application.png b/_images/components/workflow/job_application.png new file mode 100644 index 00000000000..9c5e6792ae9 Binary files /dev/null and b/_images/components/workflow/job_application.png differ diff --git a/_images/components/workflow/pull_request.png b/_images/components/workflow/pull_request.png new file mode 100644 index 00000000000..3b98078099a Binary files /dev/null and b/_images/components/workflow/pull_request.png differ diff --git a/_images/components/workflow/simple.png b/_images/components/workflow/simple.png new file mode 100644 index 00000000000..ed158d5cc7a Binary files /dev/null and b/_images/components/workflow/simple.png differ diff --git a/_images/components/workflow/states_transitions.png b/_images/components/workflow/states_transitions.png new file mode 100644 index 00000000000..1e68f9ca597 Binary files /dev/null and b/_images/components/workflow/states_transitions.png differ diff --git a/components/workflow.rst b/components/workflow.rst index 656d37f2e65..367bff2d41d 100644 --- a/components/workflow.rst +++ b/components/workflow.rst @@ -5,8 +5,8 @@ The Workflow Component ====================== - The Workflow component provides tools for managing a workflow or finite state - machine. + The Workflow component provides tools for managing a workflow or finite + state machine. .. versionadded:: 3.2 The Workflow component was introduced in Symfony 3.2. @@ -19,6 +19,87 @@ You can install the component in 2 different ways: * :doc:`Install it via Composer ` (``symfony/workflow`` on `Packagist`_); * Use the official Git repository (https://github.com/symfony/workflow). -For more information, see the code in the Git Repository. +.. include:: /components/require_autoload.rst.inc + +Creating a Workflow +------------------- + +The workflow component gives you an object oriented way to define a process +or a life cycle that your object goes through. Each step or stage in the +process is called a *place*. You do also define *transitions* that describe +the action to get from one place to another. + +.. image:: /_images/components/workflow/states_transitions.png + +A set of places and transitions creates a **definition**. A workflow needs +a ``Definition`` and a way to write the states to the objects (i.e. an +instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`). + +Consider the following example for a blog post. A post can have one of a number +of predefined statuses (`draft`, `review`, `rejected`, `published`). In a workflow, +these statuses are called **places**. You can define the workflow like this:: + + use Symfony\Component\Workflow\DefinitionBuilder; + use Symfony\Component\Workflow\Transition; + use Symfony\Component\Workflow\Workflow; + use Symfony\Component\Workflow\MarkingStore\SingleStateMarkingStore; + + $builder = new DefinitionBuilder(); + $builder->addPlaces(['draft', 'review', 'rejected', 'published']); + + // Transitions are defined with a unique name, an origin place and a destination place + $builder->addTransition(new Transition('to_review', 'draft', 'review')); + $builder->addTransition(new Transition('publish', 'review', 'published')); + $builder->addTransition(new Transition('reject', 'review', 'rejected')); + + $definition = $builder->build(); + + $marking = new SingleStateMarkingStore('currentState'); + $workflow = new Workflow($definition, $marking); + +The ``Workflow`` can now help you to decide what actions are allowed +on a blog post depending on what *place* it is in. This will keep your domain +logic in one place and not spread all over your application. + +When you define multiple workflows you should consider using a ``Registry``, +which is an object that stores and provides access to different workflows. +A registry will also help you to decide if a workflow supports the object you +are trying to use it with:: + + use Symfony\Component\Workflow\Registry; + use Acme\Entity\BlogPost; + use Acme\Entity\Newsletter; + + $blogWorkflow = ... + $newsletterWorkflow = ... + + $registry = new Registry(); + $registry->add($blogWorkflow, BlogPost::class); + $registry->add($newsletterWorkflow, Newsletter::class); + +Usage +----- + +When you have configured a ``Registry`` with your workflows, you may use it as follows:: + + // ... + $post = new BlogPost(); + $workflow = $registry->get($post); + + $workflow->can($post, 'publish'); // False + $workflow->can($post, 'to_review'); // True + + $workflow->apply($post, 'to_review'); + $workflow->can($post, 'publish'); // True + $workflow->getEnabledTransitions($post); // ['publish', 'reject'] + +Learn more +---------- + +.. toctree:: + :maxdepth: 1 + :glob: + + /workflow/* .. _Packagist: https://packagist.org/packages/symfony/workflow diff --git a/index.rst b/index.rst index eed3f3f5200..47468efcfc8 100644 --- a/index.rst +++ b/index.rst @@ -62,6 +62,7 @@ Topics testing translation validation + workflow Best Practices -------------- diff --git a/workflow.rst b/workflow.rst new file mode 100644 index 00000000000..766c8ab2a7c --- /dev/null +++ b/workflow.rst @@ -0,0 +1,50 @@ +Workflow +======== + +A workflow is a model of a process in your application. It may be the process +of how a blog post goes from draft, review and publish. Another example is when +a user submits a series of different forms to complete a task. Such processes are +best kept away from your models and should be defined in configuration. + +A **definition** of a workflow consist of places and actions to get from one +place to another. The actions are called **transistions**. A workflow does also +need to know each object's position in the workflow. That **marking store** writes +to a property of the object to remember the current place. + +.. note:: + + The terminology above is commonly used when discussing workflows and + `Petri nets`_ + +The Workflow component does also support state machines. A state machine is a subset +of a workflow and its purpose is to hold a state of your model. Read more about the +differences and specific features of state machine in :doc:`/workflow/state-machines`. + +Examples +-------- + +The simples workflow looks like this. It contains two places and one transition. + +.. image:: /_images/components/workflow/simple.png + +Workflows could be more complicated when they describe a real business case. The +workflow below describes the process to fill in a job application. + +.. image:: /_images/components/workflow/job_application.png + +When you fill in a job application in this example there are 4 to 7 steps depending +on the what job you are applying for. Some jobs require personality tests, logic tests +and/or formal requirements to be answered by the user. Some jobs don't. The +``GuardEvent`` is used to decide what next steps are allowed for a specific application. + +By defining a workflow like this, there is an overview how the process looks like. The process +logic is not mixed with the controllers, models or view. The order of the steps can be changed +by changing the configuration only. + +.. toctree:: + :maxdepth: 1 + :glob: + + workflow/* + +.. _Petri nets: https://en.wikipedia.org/wiki/Petri_net diff --git a/workflow/dumping-workflows.rst b/workflow/dumping-workflows.rst new file mode 100644 index 00000000000..56b1d1f7d56 --- /dev/null +++ b/workflow/dumping-workflows.rst @@ -0,0 +1,37 @@ +.. index:: + single: Workflow; Dumping Workflows + +How to Dump Workflows +===================== + +To help you debug your workflows, you can dump a representation of your workflow with +the use of a ``DumperInterface``. Use the ``GraphvizDumper`` to create a +PNG image of the workflow defined above:: + + // dump-graph.php + $dumper = new GraphvizDumper(); + echo $dumper->dump($definition); + +.. code-block:: terminal + + $ php dump-graph.php > out.dot + $ dot -Tpng out.dot -o graph.png + +The result will look like this: + +.. image:: /_images/components/workflow/blogpost.png + +If you have configured your workflow with the Symfony framework, you may dump the dot file +with the ``WorkflowDumpCommand``: + +.. code-block:: terminal + + $ php bin/console workflow:dump name > out.dot + $ dot -Tpng out.dot -o graph.png + +.. note:: + + The ``dot`` command is part of Graphviz. You can download it and read + more about it on `Graphviz.org`_. + +.. _Graphviz.org: http://www.graphviz.org diff --git a/workflow/state-machines.rst b/workflow/state-machines.rst new file mode 100644 index 00000000000..6139e696be8 --- /dev/null +++ b/workflow/state-machines.rst @@ -0,0 +1,197 @@ +.. index:: + single: Workflow; Workflows as State Machines + +Workflows as State Machines +=========================== + +The workflow component is modelled after a *Workflow net* which is a subclass +of a `Petri net`_. By adding further restrictions you can get a state machine. +The most important one being that a state machine cannot be in more than +one place simultaneously. It is also worth noting that a workflow does not +commonly have cyclic path in the definition graph, but it is common for a state +machine. + +Example of a State Machine +-------------------------- + +A pull request starts in an intial "start" state, a state for e.g. running +tests on Travis. When this is finished, the pull request is in the "review" +state, where contributors can require changes, reject or accept the +pull request. At any time, you can also "update" the pull request, which +will result in another Travis run. + +.. image:: /_images/components/workflow/pull_request.png + +Below is the configuration for the pull request state machine. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + framework: + workflows: + pull_request: + type: 'state_machine' + supports: + - AppBundle\Entity\PullRequest + places: + - start + - coding + - travis + - review + - merged + - closed + transitions: + submit: + from: start + to: travis + update: + from: [coding, travis, review] + to: travis + wait_for_reivew: + from: travis + to: review + request_change: + from: review + to: coding + accept: + from: review + to: merged + reject: + from: review + to: closed + reopen: + from: closed + to: review + + .. code-block:: xml + + + + + + + + + + AppBundle\Entity\PullRequest + + start + coding + travis + review + merged + closed + + + start + + travis + + + + coding + travis + review + + travis + + + + travis + + review + + + + review + + coding + + + + review + + merged + + + + review + + closed + + + + closed + + review + + + + + + + + .. code-block:: php + + // app/config/config.php + + $container->loadFromExtension('framework', array( + // ... + 'workflows' => array( + 'pull_request' => array( + 'type' => 'state_machine', + 'supports' => array('AppBundle\Entity\PullRequest'), + 'places' => array( + 'start', + 'coding', + 'travis', + 'review', + 'merged', + 'closed', + ), + 'transitions' => array( + 'start'=> array( + 'form' => 'start', + 'to' => 'travis', + ), + 'update'=> array( + 'form' => array('coding','travis','review'), + 'to' => 'travis', + ), + 'wait_for_reivew'=> array( + 'form' => 'travis', + 'to' => 'review', + ), + 'request_change'=> array( + 'form' => 'review', + 'to' => 'coding', + ), + 'accept'=> array( + 'form' => 'review', + 'to' => 'merged', + ), + 'reject'=> array( + 'form' => 'review', + 'to' => 'closed', + ), + 'reopen'=> array( + 'form' => 'start', + 'to' => 'review', + ), + ), + ), + ), + )); + +You can now use this state machine by getting the ``state_machine.pull_request`` service:: + + $stateMachine = $this->container->get('state_machine.pull_request'); + +.. _Petri net: https://en.wikipedia.org/wiki/Petri_net diff --git a/workflow/usage.rst b/workflow/usage.rst new file mode 100644 index 00000000000..a47743ca458 --- /dev/null +++ b/workflow/usage.rst @@ -0,0 +1,244 @@ +.. index:: + single: Workflow; Usage + +How to Use the Workflow +======================= + +A workflow is a process or a lifecycle that your objects go through. Each +step or stage in the process is called a *place*. You do also define *transitions* +to that describes the action to get from one place to another. + +.. image:: /_images/components/workflow/states_transitions.png + +A set of places and transitions creates a **definition**. A workflow needs +a ``Definition`` and a way to write the states to the objects (i.e. an +instance of a :class:`Symfony\\Component\\Workflow\\MarkingStore\\MarkingStoreInterface`.) + +Consider the following example for a blog post. A post can have places: +'draft', 'review', 'rejected', 'published'. You can define the workflow +like this: + +.. configuration-block:: + + .. code-block:: yaml + + framework: + workflows: + blog_publishing: + type: 'workflow' # or 'state_machine' + marking_store: + type: 'multiple_state' # or 'single_state' + arguments: + - 'currentPlace' + supports: + - AppBundle\Entity\BlogPost + places: + - draft + - review + - rejected + - published + transitions: + to_review: + from: draft + to: review + publish: + from: review + to: published + reject: + from: review + to: rejected + + .. code-block:: xml + + + + + + + + + currentPlace + + + AppBundle\Entity\BlogPost + + draft + review + rejected + published + + + draft + + review + + + + review + + published + + + + review + + rejected + + + + + + + + .. code-block:: php + + // app/config/config.php + + $container->loadFromExtension('framework', array( + // ... + 'workflows' => array( + 'blog_publishing' => array( + 'type' => 'workflow', // or 'state_machine' + 'marking_store' => array( + 'type' => 'multiple_state', // or 'single_state' + 'arguments' => array('currentPlace') + ), + 'supports' => array('AppBundle\Entity\BlogPost'), + 'places' => array( + 'draft', + 'review', + 'rejected', + 'published', + ), + 'transitions' => array( + 'to_review'=> array( + 'form' => 'draft', + 'to' => 'review', + ), + 'publish'=> array( + 'form' => 'review', + 'to' => 'published', + ), + 'reject'=> array( + 'form' => 'review', + 'to' => 'rejected', + ), + ), + ), + ), + )); + +.. code-block:: php + + class BlogPost + { + // This property is used by the marking store + public $currentPlace; + public $title; + public $content + } + +.. note:: + + The marking store type could be "multiple_state" or "single_state". + A single state marking store does not support a model being on multiple places + at the same time. + +With this workflow named ``blog_publishing``, you can get help to decide +what actions that are allowed on a blog post. :: + + $post = new \AppBundle\Entity\BlogPost(); + + $workflow = $this->container->get('workflow.blog_publishing'); + $workflow->can($post, 'publish'); // False + $workflow->can($post, 'to_review'); // True + + // Update the currentState on the post + try { + $workflow->apply($post, 'to_review'); + } catch (LogicException $e) { + // ... + } + + // See all the available transition for the post in the current state + $transitions = $workflow->getEnabledTransitions($post); + +Using Events +------------ + +To make your workflows even more powerful you could construct the ``Workflow`` +object with an ``EventDispatcher``. You can now create event listeners to +block transitions (i.e. depending on the data in the blog post). The following +events are dispatched: + +* ``workflow.guard`` +* ``workflow.[workflow name].guard`` +* ``workflow.[workflow name].guard.[transition name]`` + +See example to make sure no blog post without title is moved to "review":: + + use Symfony\Component\Workflow\Event\GuardEvent; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class BlogPostReviewListener implements EventSubscriberInterface + { + public function guardReview(GuardEvent $event) + { + /** @var Acme\BlogPost $post */ + $post = $event->getSubject(); + $title = $post->title; + + if (empty($title)) { + // Posts with no title should not be allowed + $event->setBlocked(true); + } + } + + public static function getSubscribedEvents() + { + return array( + 'workflow.blogpost.guard.to_review' => array('guardReview'), + ); + } + } + +With help from the ``EventDispatcher`` and the ``AuditTrailListener`` you +could easily enable logging:: + + use Symfony\Component\Workflow\EventListener\AuditTrailListener; + + $logger = new AnyPsr3Logger(); + $subscriber = new AuditTrailListener($logger); + $dispatcher->addSubscriber($subscriber); + +Usage in Twig +------------- + +Using your workflow in your Twig templates reduces the need of domain logic +in the view layer. Consider this example of the control panel of the blog. +The links below will only be displayed when the action is allowed: + +.. code-block:: twig + +

Actions

+ {% if workflow_can(post, 'publish') %} + Publish article + {% endif %} + {% if workflow_can(post, 'to_review') %} + Submit to review + {% endif %} + {% if workflow_can(post, 'reject') %} + Reject article + {% endif %} + + {# Or loop through the enabled transistions #} + {% for transition in workflow_transitions(post) %} + {{ transition.name }} + {% else %} + No actions available. + {% endfor %}