diff --git a/_build/redirection_map b/_build/redirection_map index 8b0b30eb132..2406f17b965 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -362,3 +362,5 @@ /console/logging /console /frontend/encore/legacy-apps /frontend/encore/legacy-applications /contributing/code/patches /contributing/code/pull_requests +/workflow/state-machines /workflow/introduction +/workflow/usage /workflow diff --git a/reference/configuration/framework.rst b/reference/configuration/framework.rst index 2e88b39b9f4..de74aedfc8c 100644 --- a/reference/configuration/framework.rst +++ b/reference/configuration/framework.rst @@ -2306,8 +2306,9 @@ type **type**: ``string`` **possible values**: ``'workflow'`` or ``'state_machine'`` -Defines the kind fo workflow that is going to be created, which can be either -a :doc:`normal workflow ` or a :doc:`state machine `. +Defines the kind of workflow that is going to be created, which can be either +a normal workflow or a state machine. Read :doc:`this article ` +to know their differences. .. _`HTTP Host header attacks`: http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html .. _`Security Advisory Blog post`: https://symfony.com/blog/security-releases-symfony-2-0-24-2-1-12-2-2-5-and-2-3-3-released#cve-2013-4752-request-gethost-poisoning diff --git a/workflow.rst b/workflow.rst index 8e4cba3b5a9..530e318d609 100644 --- a/workflow.rst +++ b/workflow.rst @@ -1,50 +1,499 @@ 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. +Using the Workflow component inside a Symfony application requires to know first +some basic theory and concepts about workflows and state machines. +:doc:`Read this article ` for a quick overview. -A **definition** of a workflow consist of places and actions to get from one -place to another. The actions are called **transitions**. 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. +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 these places: +``draft``, ``reviewed``, ``rejected``, ``published``. You can define the workflow +like this: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + framework: + workflows: + blog_publishing: + type: 'workflow' # or 'state_machine' + audit_trail: + enabled: true + marking_store: + type: 'multiple_state' # or 'single_state' + arguments: + - 'currentPlace' + supports: + - AppBundle\Entity\BlogPost + initial_place: draft + places: + - draft + - reviewed + - rejected + - published + transitions: + to_review: + from: draft + to: reviewed + publish: + from: reviewed + to: published + reject: + from: reviewed + to: rejected + + .. code-block:: xml + + + + + + + + + + + currentPlace + + + AppBundle\Entity\BlogPost + + draft + reviewed + rejected + published + + + draft + + reviewed + + + + reviewed + + published + + + + reviewed + + rejected + + + + + + + + .. code-block:: php + + // app/config/config.php + $container->loadFromExtension('framework', [ + // ... + 'workflows' => [ + 'blog_publishing' => [ + 'type' => 'workflow', // or 'state_machine' + 'audit_trail' => [ + 'enabled' => true + ], + 'marking_store' => [ + 'type' => 'multiple_state', // or 'single_state' + 'arguments' => ['currentPlace'] + ], + 'supports' => ['AppBundle\Entity\BlogPost'], + 'places' => [ + 'draft', + 'reviewed', + 'rejected', + 'published', + ], + 'transitions' => [ + 'to_review' => [ + 'from' => 'draft', + 'to' => 'reviewed', + ], + 'publish' => [ + 'from' => 'reviewed', + 'to' => 'published', + ], + 'reject' => [ + 'from' => 'reviewed', + 'to' => 'rejected', + ], + ], + ], + ], + ]); + +.. tip:: + + If you are creating your first workflows, consider using the ``workflow:dump`` + command to :doc:`debug the workflow contents `. + +As configured, the following property is used by the marking store:: + + 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. + +.. tip:: + + The ``type`` (default value ``single_state``) and ``arguments`` (default + value ``marking``) attributes of the ``marking_store`` option are optional. + If omitted, their default values will be used. + +.. tip:: + + Setting the ``audit_trail.enabled`` option to ``true`` makes the application + generate detailed log messages for the workflow activity. + + .. versionadded:: 3.3 + + The ``audit_trail`` option was introduced in Symfony 3.3. + +With this workflow named ``blog_publishing``, you can get help to decide +what actions 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 $exception) { + // ... + } + + // See all the available transitions for the post in the current state + $transitions = $workflow->getEnabledTransitions($post); + +Using Events +------------ + +To make your workflows more flexible, you can 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) and do +additional actions when a workflow operation happened (e.g. sending +announcements). + +Each step has three events that are fired in order: + +* An event for every workflow; +* An event for the workflow concerned; +* An event for the workflow concerned with the specific transition or place name. + +When a state transition is initiated, the events are dispatched in the following +order: + +``workflow.guard`` + Validate whether the transition is blocked or not (see + :ref:`guard events ` and + :ref:`blocking transitions `). + + The three events being dispatched are: + + * ``workflow.guard`` + * ``workflow.[workflow name].guard`` + * ``workflow.[workflow name].guard.[transition name]`` + +``workflow.leave`` + The subject is about to leave a place. + + The three events being dispatched are: + + * ``workflow.leave`` + * ``workflow.[workflow name].leave`` + * ``workflow.[workflow name].leave.[place name]`` + +``workflow.transition`` + The subject is going through this transition. + + The three events being dispatched are: + + * ``workflow.transition`` + * ``workflow.[workflow name].transition`` + * ``workflow.[workflow name].transition.[transition name]`` + +``workflow.enter`` + The subject is about to enter a new place. This event is triggered just + before the subject places are updated, which means that the marking of the + subject is not yet updated with the new places. + + The three events being dispatched are: + + * ``workflow.enter`` + * ``workflow.[workflow name].enter`` + * ``workflow.[workflow name].enter.[place name]`` + +``workflow.entered`` + The subject has entered in the places and the marking is updated (making it a good + place to flush data in Doctrine). + + The three events being dispatched are: + + * ``workflow.entered`` + * ``workflow.[workflow name].entered`` + * ``workflow.[workflow name].entered.[place name]`` + +``workflow.completed`` + The object has completed this transition. + + The three events being dispatched are: + + * ``workflow.completed`` + * ``workflow.[workflow name].completed`` + * ``workflow.[workflow name].completed.[transition name]`` + + +``workflow.announce`` + Triggered for each transition that now is accessible for the subject. + + The three events being dispatched are: + + * ``workflow.announce`` + * ``workflow.[workflow name].announce`` + * ``workflow.[workflow name].announce.[transition name]`` .. note:: - The terminology above is commonly used when discussing workflows and - `Petri nets`_ + The leaving and entering events are triggered even for transitions that stay + in same place. + +Here is an example of how to enable logging for every time a "blog_publishing" +workflow leaves a place:: + + use Psr\Log\LoggerInterface; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Workflow\Event\Event; + + class WorkflowLogger implements EventSubscriberInterface + { + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function onLeave(Event $event) + { + $this->logger->alert(sprintf( + 'Blog post (id: "%s") performed transition "%s" from "%s" to "%s"', + $event->getSubject()->getId(), + $event->getTransition()->getName(), + implode(', ', array_keys($event->getMarking()->getPlaces())), + implode(', ', $event->getTransition()->getTos()) + )); + } + + public static function getSubscribedEvents() + { + return [ + 'workflow.blog_publishing.leave' => 'onLeave', + ]; + } + } + +.. _workflow-usage-guard-events: + +Guard Events +~~~~~~~~~~~~ + +There are a special kind of events called "Guard events". Their event listeners +are invoked every time a call to ``Workflow::can``, ``Workflow::apply`` or +``Workflow::getEnabledTransitions`` is executed. With the guard events you may +add custom logic to decide which transitions should be blocked or not. Here is a +list of the guard event names. + +* ``workflow.guard`` +* ``workflow.[workflow name].guard`` +* ``workflow.[workflow name].guard.[transition name]`` + +This example stops any blog post being transitioned to "reviewed" if it is +missing a title:: + + use Symfony\Component\Workflow\Event\GuardEvent; + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class BlogPostReviewListener implements EventSubscriberInterface + { + public function guardReview(GuardEvent $event) + { + /** @var AppBundle\Entity\BlogPost $post */ + $post = $event->getSubject(); + $title = $post->title; + + if (empty($title)) { + // Block the transition "to_review" if the post has no title + $event->setBlocked(true); + } + } -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`. + public static function getSubscribedEvents() + { + return [ + 'workflow.blog_publishing.guard.to_review' => ['guardReview'], + ]; + } + } -Examples --------- +Event Methods +~~~~~~~~~~~~~ -The simplest workflow looks like this. It contains two places and one transition. +Each workflow event is an instance of :class:`Symfony\\Component\\Workflow\\Event\\Event`. +This means that each event has access to the following information: -.. image:: /_images/components/workflow/simple.png +:method:`Symfony\\Component\\Workflow\\Event\\Event::getMarking` + Returns the :class:`Symfony\\Component\\Workflow\\Marking` of the workflow. -Workflows could be more complicated when they describe a real business case. The -workflow below describes the process to fill in a job application. +:method:`Symfony\\Component\\Workflow\\Event\\Event::getSubject` + Returns the object that dispatches the event. -.. image:: /_images/components/workflow/job_application.png +:method:`Symfony\\Component\\Workflow\\Event\\Event::getTransition` + Returns the :class:`Symfony\\Component\\Workflow\\Transition` that dispatches the event. -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. +:method:`Symfony\\Component\\Workflow\\Event\\Event::getWorkflowName` + Returns a string with the name of the workflow that triggered the event. -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. + .. versionadded:: 3.3 + + The ``getWorkflowName()`` method was introduced in Symfony 3.3. + +For Guard Events, there is an extended class :class:`Symfony\\Component\\Workflow\\Event\\GuardEvent`. +This class has two more methods: + +:method:`Symfony\\Component\\Workflow\\Event\\GuardEvent::isBlocked` + Returns if transition is blocked. + +:method:`Symfony\\Component\\Workflow\\Event\\GuardEvent::setBlocked` + Sets the blocked value. + +.. _workflow-blocking-transitions: + +Blocking Transitions +-------------------- + +The execution of the workflow can be controlled by executing custom logic to +decide if the current transition is blocked or allowed before applying it. This +feature is provided by "guards", which can be used in two ways. + +First, you can listen to :ref:`the guard events `. +Alternatively, you can define a ``guard`` configuration option for the +transition. The value of this option is any valid expression created with the +:doc:`ExpressionLanguage component `: + +.. code-block:: yaml + + # config/packages/workflow.yaml + framework: + workflows: + blog_publishing: + # previous configuration + transitions: + to_review: + # the transition is allowed only if the current user has the ROLE_REVIEWER role. + guard: "is_granted('ROLE_REVIEWER')" + from: draft + to: reviewed + publish: + # or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted" + guard: "is_authenticated" + from: reviewed + to: published + reject: + # or any valid expression language with "subject" referring to the post + guard: "has_role("ROLE_ADMIN") and subject.isStatusReviewed()" + from: reviewed + to: rejected + +Usage in Twig +------------- + +Symfony defines several Twig functions to manage workflows and reduce the need +of domain logic in your templates: + +``workflow_can()`` + Returns ``true`` if the given object can make the given transition. + +``workflow_transitions()`` + Returns an array with all the transitions enabled for the given object. + +``workflow_marked_places()`` + Returns an array with the place names of the given marking. + +``workflow_has_marked_place()`` + Returns ``true`` if the marking of the given object has the given state. + +.. versionadded:: 3.3 + + The ``workflow_marked_places()`` and ``workflow_has_marked_place()`` + functions were introduced in Symfony 3.3. + +The following example shows these functions in action: + +.. code-block:: html+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 transitions #} + {% for transition in workflow_transitions(post) %} + {{ transition.name }} + {% else %} + No actions available. + {% endfor %} + + {# Check if the object is in some specific place #} + {% if workflow_has_marked_place(post, 'reviewed') %} +

This post is ready for review.

+ {% endif %} + + {# Check if some place has been marked on the object #} + {% if 'waiting_some_approval' in workflow_marked_places(post) %} + PENDING + {% endif %} + +Learn more +---------- .. toctree:: :maxdepth: 1 - :glob: - - workflow/* -.. _Petri nets: https://en.wikipedia.org/wiki/Petri_net + workflow/introduction + workflow/dumping-workflows diff --git a/workflow/state-machines.rst b/workflow/introduction.rst similarity index 74% rename from workflow/state-machines.rst rename to workflow/introduction.rst index e1cb97e62b0..4836a39d2ec 100644 --- a/workflow/state-machines.rst +++ b/workflow/introduction.rst @@ -1,18 +1,62 @@ -.. index:: - single: Workflow; Workflows as State Machines +Workflows and State Machines +============================ -Workflows as State Machines -=========================== +Worflows +-------- -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. +A workflow is a model of a process in your application. It may be the process of +how a blog post goes from draft to 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. -Example of a State Machine --------------------------- +A **definition** of a workflow consist of places and actions to get from one +place to another. The actions are called **transitions**. 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`_ + +Examples +~~~~~~~~ + +The simplest 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. + +State Machines +-------------- + +A state machine is a subset of a workflow and its purpose is to hold a state of +your model. The most important differences between them are: + +* Workflows can be in more than one place at the same time, whereas state + machines can't; +* Workflows usually don't have cyclic paths in the definition graph, but it's + common for state machines; +* In order to apply a transition, worflows require that the object is in all the + previous places of the transition, whereas state machines only require that + the object is at least in one of those places. + +Example +~~~~~~~ A pull request starts in an initial "start" state, a state for e.g. running tests on Travis. When this is finished, the pull request is in the "review" @@ -190,8 +234,9 @@ Below is the configuration for the pull request state machine. ], ]); -You can now use this state machine by getting the ``state_machine.pull_request`` service:: +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 +.. _`Petri nets`: https://en.wikipedia.org/wiki/Petri_net diff --git a/workflow/usage.rst b/workflow/usage.rst deleted file mode 100644 index e8f1268dc14..00000000000 --- a/workflow/usage.rst +++ /dev/null @@ -1,486 +0,0 @@ -.. 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 these places: -``draft``, ``reviewed``, ``rejected``, ``published``. You can define the workflow -like this: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/config.yml - framework: - workflows: - blog_publishing: - type: 'workflow' # or 'state_machine' - audit_trail: - enabled: true - marking_store: - type: 'multiple_state' # or 'single_state' - arguments: - - 'currentPlace' - supports: - - AppBundle\Entity\BlogPost - initial_place: draft - places: - - draft - - reviewed - - rejected - - published - transitions: - to_review: - from: draft - to: reviewed - publish: - from: reviewed - to: published - reject: - from: reviewed - to: rejected - - .. code-block:: xml - - - - - - - - - - - currentPlace - - - AppBundle\Entity\BlogPost - - draft - reviewed - rejected - published - - - draft - - reviewed - - - - reviewed - - published - - - - reviewed - - rejected - - - - - - - - .. code-block:: php - - // app/config/config.php - $container->loadFromExtension('framework', [ - // ... - 'workflows' => [ - 'blog_publishing' => [ - 'type' => 'workflow', // or 'state_machine' - 'audit_trail' => [ - 'enabled' => true - ], - 'marking_store' => [ - 'type' => 'multiple_state', // or 'single_state' - 'arguments' => ['currentPlace'] - ], - 'supports' => ['AppBundle\Entity\BlogPost'], - 'places' => [ - 'draft', - 'reviewed', - 'rejected', - 'published', - ], - 'transitions' => [ - 'to_review' => [ - 'from' => 'draft', - 'to' => 'reviewed', - ], - 'publish' => [ - 'from' => 'reviewed', - 'to' => 'published', - ], - 'reject' => [ - 'from' => 'reviewed', - 'to' => 'rejected', - ], - ], - ], - ], - ]); - -As configured, the following property is used by the marking store:: - - 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. - -.. tip:: - - The ``type`` (default value ``single_state``) and ``arguments`` (default value ``marking``) - attributes of the ``marking_store`` option are optional. If omitted, their default values - will be used. - -.. tip:: - - Setting the ``audit_trail.enabled`` option to ``true`` makes the application - generate detailed log messages for the workflow activity. - - .. versionadded:: 3.3 - - The ``audit_trail`` option was introduced in Symfony 3.3. - -With this workflow named ``blog_publishing``, you can get help to decide -what actions 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 $exception) { - // ... - } - - // See all the available transitions for the post in the current state - $transitions = $workflow->getEnabledTransitions($post); - -Using Events ------------- - -To make your workflows more flexible, you can 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) and do -additional actions when a workflow operation happened (e.g. sending -announcements). - -Each step has three events that are fired in order: - -* An event for every workflow; -* An event for the workflow concerned; -* An event for the workflow concerned with the specific transition or place name. - -When a state transition is initiated, the events are dispatched in the following -order: - -``workflow.guard`` - Validate whether the transition is blocked or not (see - :ref:`guard events ` and - :ref:`blocking transitions `). - - The three events being dispatched are: - - * ``workflow.guard`` - * ``workflow.[workflow name].guard`` - * ``workflow.[workflow name].guard.[transition name]`` - -``workflow.leave`` - The subject is about to leave a place. - - The three events being dispatched are: - - * ``workflow.leave`` - * ``workflow.[workflow name].leave`` - * ``workflow.[workflow name].leave.[place name]`` - -``workflow.transition`` - The subject is going through this transition. - - The three events being dispatched are: - - * ``workflow.transition`` - * ``workflow.[workflow name].transition`` - * ``workflow.[workflow name].transition.[transition name]`` - -``workflow.enter`` - The subject is about to enter a new place. This event is triggered just - before the subject places are updated, which means that the marking of the - subject is not yet updated with the new places. - - The three events being dispatched are: - - * ``workflow.enter`` - * ``workflow.[workflow name].enter`` - * ``workflow.[workflow name].enter.[place name]`` - -``workflow.entered`` - The subject has entered in the places and the marking is updated (making it a good - place to flush data in Doctrine). - - The three events being dispatched are: - - * ``workflow.entered`` - * ``workflow.[workflow name].entered`` - * ``workflow.[workflow name].entered.[place name]`` - -``workflow.completed`` - The object has completed this transition. - - The three events being dispatched are: - - * ``workflow.completed`` - * ``workflow.[workflow name].completed`` - * ``workflow.[workflow name].completed.[transition name]`` - - -``workflow.announce`` - Triggered for each transition that now is accessible for the subject. - - The three events being dispatched are: - - * ``workflow.announce`` - * ``workflow.[workflow name].announce`` - * ``workflow.[workflow name].announce.[transition name]`` - -.. note:: - - The leaving and entering events are triggered even for transitions that stay - in same place. - -Here is an example of how to enable logging for every time a "blog_publishing" -workflow leaves a place:: - - use Psr\Log\LoggerInterface; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - use Symfony\Component\Workflow\Event\Event; - - class WorkflowLogger implements EventSubscriberInterface - { - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - public function onLeave(Event $event) - { - $this->logger->alert(sprintf( - 'Blog post (id: "%s") performed transition "%s" from "%s" to "%s"', - $event->getSubject()->getId(), - $event->getTransition()->getName(), - implode(', ', array_keys($event->getMarking()->getPlaces())), - implode(', ', $event->getTransition()->getTos()) - )); - } - - public static function getSubscribedEvents() - { - return [ - 'workflow.blog_publishing.leave' => 'onLeave', - ]; - } - } - -.. _workflow-usage-guard-events: - -Guard Events -~~~~~~~~~~~~ - -There are a special kind of events called "Guard events". Their event listeners -are invoked every time a call to ``Workflow::can``, ``Workflow::apply`` or -``Workflow::getEnabledTransitions`` is executed. With the guard events you may -add custom logic to decide which transitions should be blocked or not. Here is a list -of the guard event names. - -* ``workflow.guard`` -* ``workflow.[workflow name].guard`` -* ``workflow.[workflow name].guard.[transition name]`` - -This example stops any blog post being transitioned to "reviewed" if it is -missing a title:: - - use Symfony\Component\Workflow\Event\GuardEvent; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; - - class BlogPostReviewListener implements EventSubscriberInterface - { - public function guardReview(GuardEvent $event) - { - /** @var AppBundle\Entity\BlogPost $post */ - $post = $event->getSubject(); - $title = $post->title; - - if (empty($title)) { - // Block the transition "to_review" if the post has no title - $event->setBlocked(true); - } - } - - public static function getSubscribedEvents() - { - return [ - 'workflow.blog_publishing.guard.to_review' => ['guardReview'], - ]; - } - } - -Event Methods -~~~~~~~~~~~~~ - -Each workflow event is an instance of :class:`Symfony\\Component\\Workflow\\Event\\Event`. -This means that each event has access to the following information: - -:method:`Symfony\\Component\\Workflow\\Event\\Event::getMarking` - Returns the :class:`Symfony\\Component\\Workflow\\Marking` of the workflow. - -:method:`Symfony\\Component\\Workflow\\Event\\Event::getSubject` - Returns the object that dispatches the event. - -:method:`Symfony\\Component\\Workflow\\Event\\Event::getTransition` - Returns the :class:`Symfony\\Component\\Workflow\\Transition` that dispatches the event. - -:method:`Symfony\\Component\\Workflow\\Event\\Event::getWorkflowName` - Returns a string with the name of the workflow that triggered the event. - - .. versionadded:: 3.3 - - The ``getWorkflowName()`` method was introduced in Symfony 3.3. - -For Guard Events, there is an extended class :class:`Symfony\\Component\\Workflow\\Event\\GuardEvent`. -This class has two more methods: - -:method:`Symfony\\Component\\Workflow\\Event\\GuardEvent::isBlocked` - Returns if transition is blocked. - -:method:`Symfony\\Component\\Workflow\\Event\\GuardEvent::setBlocked` - Sets the blocked value. - -.. _workflow-blocking-transitions: - -Blocking Transitions --------------------- - -The execution of the workflow can be controlled by executing custom logic to -decide if the current transition is blocked or allowed before applying it. This -feature is provided by "guards", which can be used in two ways. - -First, you can listen to :ref:`the guard events `. -Alternatively, you can define a ``guard`` configuration option for the -transition. The value of this option is any valid expression created with the -:doc:`ExpressionLanguage component `: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/workflow.yaml - framework: - workflows: - blog_publishing: - # previous configuration - transitions: - to_review: - # the transition is allowed only if the current user has the ROLE_REVIEWER role. - guard: "is_granted('ROLE_REVIEWER')" - from: draft - to: reviewed - publish: - # or "is_anonymous", "is_remember_me", "is_fully_authenticated", "is_granted" - guard: "is_authenticated" - from: reviewed - to: published - reject: - # or any valid expression language with "subject" referring to the post - guard: "has_role("ROLE_ADMIN") and subject.isStatusReviewed()" - from: reviewed - to: rejected - -Usage in Twig -------------- - -Symfony defines several Twig functions to manage workflows and reduce the need -of domain logic in your templates: - -``workflow_can()`` - Returns ``true`` if the given object can make the given transition. - -``workflow_transitions()`` - Returns an array with all the transitions enabled for the given object. - -``workflow_marked_places()`` - Returns an array with the place names of the given marking. - -``workflow_has_marked_place()`` - Returns ``true`` if the marking of the given object has the given state. - -.. versionadded:: 3.3 - - The ``workflow_marked_places()`` and ``workflow_has_marked_place()`` - functions were introduced in Symfony 3.3. - -The following example shows these functions in action: - -.. code-block:: html+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 transitions #} - {% for transition in workflow_transitions(post) %} - {{ transition.name }} - {% else %} - No actions available. - {% endfor %} - - {# Check if the object is in some specific place #} - {% if workflow_has_marked_place(post, 'reviewed') %} -

This post is ready for review.

- {% endif %} - - {# Check if some place has been marked on the object #} - {% if 'waiting_some_approval' in workflow_marked_places(post) %} - PENDING - {% endif %}