Set up workflows with State Machine on Drupal Commerce 2.x

Kevin Wenger
8 min readJan 7, 2019

--

This blog post addresses the issue of How to attach a State Machine Workflow to a Commerce Product in Drupal Commerce 2.x.

As we demonstrate in this previous post, it exists several ways to solve this problem.
However, as a strong advocate of the developer approach in Drupal 8, I will, of course, devote this paper to creating publishing workflows via the State Machine module.

This article will guide you step by step in the process of code driven Workflow creation.

Truth can only be found in one place: the code
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship

Enough history, here is 2 features covered by this post:

  • The publish status of products is managed by a State Machine.
    To summarise, the Publish button is no longer accessible, the different states of the workflow influence the state of publication.
  • React to transitions of publication states in order to apply Business Logics to them.
    For example: send an e-mail to the author of a product when it is ready for upload.

This story is based on version 2.11.x of Drupal Commerce 2.x and relies on the 8.x-1.0-beta3 of State Machine module.

Getting Started

The use of the State Machine module and the implementation of a state machine are simple:

  • Workflows must be defined in a custom module using YAML file.
    These files contain the different states (in orange on the diagram below) as well as the different transitions between the states (in blue on the same diagram).
  • The State Machine module exposes a new State field type.
    You will be able to add as many fields as needed for any entity type.
    Then we can define which workflows apply for each field independently of each other.
  • Finally, on each state change, an event is propagated by Drupal.
    Then it is possible to react to these events and perform as many actions as necessary to meet your own business logic.

Please note that you will need to apply this Drupal 8 patch to use new generic events pre and post transitions.

Workflows creation

Unlike Content Moderation and Workflows modules, State Machine workflows need to be manually declared using YAML file.

Before starting with your YAML definition file, I advise you to design your Workflow.
Here is the one we will use for this article.

We will create a new module my_workflow. This module uses the following skeleton:

├── my_workflow.info.yml   
├── my_workflow.services.yml
├── my_workflow.workflow_groups.yml
├── my_workflow.workflows.yml
├── src
│ ├── EventSubscriber
│ │ └── WorkflowTransitionEventSubscriber.php
│ ├── WorkflowHelper.php

The files that will interest us are:

  • my_workflow.workflow_groups.yml, allows the creation of workflow groups;
  • my_workflow.workflows.yml, declares and defines all the workflows of the module;
  • WorkflowTransitionEventSubscriber.php, in which we will react to different states and transitions of our workflow.

A workflow of State Machine must be part of a Workflow Group.
We will create this group (product_publication) which should be declared in the my_workflow.workflow_groups.yml file.

product_publication:
label: Publication
entity_type: commerce_product

We can now declare and define all transitions of this new workflow directly in the my_workflow.workflows.yml file.

product_publication_default:
id: product_publication_default
label: Commerce product publication
group: product_publication
states:
draft:
label: Draft
published: false
imported:
label: Imported
published: false
needs_update:
label: Needs update
published: false
validated:
label: Published
published: true
archived:
label: Archived
published: false
transitions:
save_as_draft:
label: Save as draft
from: [draft, imported]
to: draft
validate:
label: Publish
from: [needs_update]
to: validated
needs_update:
label: Request changes
from: [draft, imported, validated]
to: needs_update
unarchive:
label: Unarchive
from: [archived]
to: draft
archive:
label: Archive
from: [draft, imported, needs_update, validated]
to: archived

You need to declare a set of states — under the states key — as well as a set of transitions — under the transitions key. <br>
A quick comparison with the diagram shown above will help you to understand statements and transitions.

You may notice that I have voluntarily added a key published in some states.
These will allow me to set up a Business Logic during a state transition by relying on this property to change the publication status of a product.
We will then only need to check the presence of this property as well as its value, instead of using the name of the transition in order to manage the publish/unpublish status.

  • published: true, publish the product;
  • published: false, unpublish it.

As you can see, you can add as many properties as you need, but you will need to code the different behaviors in an EventSubscriber, which we’ll see later in this post.

You can now activate your module and configure the new State fields in your product types.

State Machine Field Configurations

1.to enable Workflow, simply add the new State field type.
In the example below, we added a Publication status field to our product type.

Add the new State field type

2. Then we can configure this field to use the available workflows — see the files my_workflow_workflow_groups.yml and my_workflow.workflows.yml.

Configure the State field to use an available workflow

Be careful, if your workflow does not appear, check the entity_type: property of your Workflow Group.

3. Finally, we can configure our Form Display** to show our State field and hide the default Published button.
As we manage in our Buisness Logic the publication states, the default action buttons is no longer needed.

Configure the Form Display

React to transitions and implement a Business Logic

Once the State fields created and attached to our workflows, all you have to do is to react on Transitions in order to perform operations.
Each State field will propagate a set of events during a transition:

  • a pre-transition event;
  • a post-transition event.

The identifiers of these events follow this pattern:

[group_id].[transition_id].[pre_transition|post_transition]

Thanks to the patch mentioned in the introduction, more generic events are also propagated, using this pattern:

state_machine.[pre_transition|post_transition]

In order to respond to these events, we will need to *mplement an EventSubscriber. The declaration for this service is in the my_workflow.services.yml file.

services:
my_workflow.workflow.helper:
class: Drupal\my_workflow\WorkflowHelper
my_workflow.workflow_transition:
class: Drupal\my_workflow\EventSubscriber\WorkflowTransitionEventSubscriber
arguments: ['@my_workflow.workflow.helper']
tags:
- { name: event_subscriber }

This file declares the following services:

  • WorkflowTransitionEventSubscriber.php, this service will contain all of our Business Logic in order to publish or unpublish a product;
  • WorkflowHelper.php this one will contain only utilitarian methods concerning the workflow management.

Let’s take a look at WorkflowTransitionEventSubscriber.php

<?phpnamespace Drupal\my_workflow\EventSubscriber;use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\my_workflow\WorkflowHelper;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowState;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\commerce_product\Entity\ProductInterface;
/**
* Event subscriber to handle actions on workflow-enabled entities.
*/
class WorkflowTransitionEventSubscriber implements EventSubscriberInterface {
/**
* The workflow helper.
*
* @var \Drupal\my_workflow\WorkflowHelper
*/
protected $workflowHelper;
/**
* Constructs a new WorkflowTransitionEventSubscriber object.
*
* @param \Drupal\my_workflow\WorkflowHelper $workflowHelper
* The workflow helper.
*/
public function __construct(WorkflowHelper $workflowHelper) {
$this->workflowHelper = $workflowHelper;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
'state_machine.pre_transition' => [
'handleAction', -180,
],
];
}
/**
* Handle action based on the workflow.
*
* @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
* The state change event.
*/
public function handleAction(WorkflowTransitionEvent $event) {
$entity = $event->getEntity();
// Don't handle action on non-commerce_product or non-publishable entity.
if (!$entity instanceof ProductInterface || !$entity instanceof EntityPublishedInterface) {
return;
}
// Verify if the new state is marked as published state.
$is_published_state = $this->isPublishedState($event->getToState(), $event->getWorkflow());
if ($is_published_state === TRUE) {
$entity->setPublished();
}
elseif ($is_published_state === FALSE) {
$entity->setUnpublished();
}
}
/**
* Checks if a state is set as published in a certain workflow.
*
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowState $state
* The state to check.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
* The workflow the state belongs to.
*
* @return bool
* TRUE if the state is set as published in the workflow, FALSE otherwise.
*/
protected function isPublishedState(WorkflowState $state, WorkflowInterface $workflow) {
return $this->workflowHelper->isWorkflowStatePublished($state->getId(), $workflow);
}
}

https://gist.github.com/WengerK/4f9e03c6019f45b956a50bf673f3cf14#file-workflowtransitioneventsubscriber-php

In this file, we subscribe to the event state_machine.pre_transition via the method getSubscribedEvents() and we call the handleAction() method. In this method we manage the release status of products.
Then, we use our published property via WorkflowHelper::isWorkflowStatePublish() which we previously associated with multiple states.
The isWorkflowStatePublished method allows us to inspect the workflow plug-in to discover the values of our published property.

<?phpnamespace Drupal\my_workflow;use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
/**
* Contains helper methods to retrieve workflow related data from entities.
*/
class WorkflowHelper {
/**
* Checks if a state is set as published in a certain workflow.
*
* @param string $state_id
* The ID of the state to check.
* @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
* The workflow the state belongs to.
*
* @return bool|null
* TRUE|FALSE if the state is set in the workflow, NULL otherwise.
*
* @throwns \InvalidArgumentException
* Thrown when the workflow is not plugin based, because this is required to
* retrieve the publication state from the workflow states.
*/
public function isWorkflowStatePublished($state_id, WorkflowInterface $workflow) {
// We rely on being able to inspect the plugin definition. Throw an error if
// this is not the case.
if (!$workflow instanceof PluginInspectionInterface) {
$label = $workflow->getLabel();
throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
}
// Retrieve the raw plugin definition, as all additional plugin settings
// are stored there.
$raw_workflow_definition = $workflow->getPluginDefinition();
return $raw_workflow_definition['states'][$state_id]['published'] ?? NULL;
}
}

In just a few lines, we can now implement any necessary reaction based on a transition and a particular status, let’s remember, on any entity of Drupal 8.

Sources

For the most curious of you, here are some sources of additional information that inspired the creation of this article.

Patrick Kenny (30 Septembre, 2017). Workflow, Workflows, Workspace, and State Machine — what is the difference?
See on https://drupal.stackexchange.com/questions/2470.../...

Kim Pepper (29 Novembre, 2017). Workflows: A new tool in the toolbox.
See on https://previousnext.com.au/blog/work../

Flocon de toile (30 Novembre, 2017). Set up workflows with State machine on Drupal 8.
See on https://flocondetoile.fr/blog/set..

Drupal Commerce. State Machine.
See on https://docs.drupalcommerce.org/commerce2/de../

Ontologia (01 Janvier 2013). Pourquoi les développeurs n’utilisent pas plus de machines à état ?
See on https://linuxfr.org/news/pourquoi../

Symfony. The EventDispatcher Component.
See on https://symfony.com/doc/current../

João Moura (29 Mars 2018). State Machine in Elixir with Machinery.
See on https://medium.com/@joaomdmoura/st../

Kevin Wenger (10 Décembre, 2018). The good, the bad and the ugly — Workflow & State Machine dans Drupal 8
See on https://medium.com/@WengerK/workflow-st…

--

--

Kevin Wenger
Kevin Wenger

Written by Kevin Wenger

Swiss Web Developer & Open Source Advocate @antistatique | @Webmardi organizer | Speaker | Author | Drupal 8 Core contributor

No responses yet