ComputerMinds.co.uk: How to: Implement an automated Commerce Order state transition
A common requirement for any website that sells products is to have a mechanism in place that ensures orders placed on the website are 'Exportable' - being made available as a file that can be sent across to a different system, to handle the processing of the order.
The Drupal Commerce 2.x module (for Drupal 9, 10) has the concept of order 'Workflows', along with defined 'States' and 'Transitions'. A workflow is a set of states and transitions that the order will go through during its lifecycle. Transitions are applied to an order and this will progress the order from one state to another. By default, a few workflows are provided but for sites that we build, we usually write our own order workflows to handle the specific requirements of the site in question.
A comparison of one of the default Commerce order workflows (on the left) and our own custom workflow (on the right). For more information about creating a custom order workflow, see the excellent documentation on drupalcommerce.org.
One common requirement that doesn’t quite work properly ‘out of the box’ is the ability for an order to be automatically transitioned to a separate 'exported' or 'completed' state, immediately after the order has been paid for successfully. We can achieve the desired functionality with a combination of an event subscriber and a useful entity update hook. Here’s what we did:
Step 1 - Implement the event subscriber.
We’ve already covered event subscribers before in a number of our articles so I won’t go into the specifics here, but the first thing we’ll want to do is create a new one that will be able to react to our Orders’ state transition.
N.B. In this example, we are assuming our custom code will live in a module that already exists, named ‘example_order_exporter’.
First off, we’ll add a new file inside of our custom module to handle our event subscriber. In the example code in this article, we’ll be naming it OrderExporterSubscriber.php and the full path it should live under is example_order_exporter/src/EventSubscriber/OrderExporterSubscriber.php
namespace Drupal\example_order_exporter\EventSubscriber;
use Drupal\example_order_exporter\OrderExporter;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderExporterSubscriber implements EventSubscriberInterface {
/**
* @var \Drupal\example_order_exporter\OrderExporter
*/
protected $orderExporter;
/**
* {@inheritDoc}
*/
public function __construct(OrderExporter $order_exporter) {
$this->orderExporter = $order_exporter;
}
/**
* The 'place' transition takes place after the order has been paid for in
* full, so we'll subscribe to the post_transition event at this point.
*/
public static function getSubscribedEvents() {
return [
'commerce_order.place.post_transition' => ['onOrderPlace', -100],
];
}
/**
* Act on an order after it has been paid for.
*
* We simply set a success / failure flag for the export here, and then
* handle the state change in a hook_commerce_order_update() implementation
* as the previous state change to payment will have finished by that point.
*
* @see example_order_exporter_commerce_order_update()
*/
public function onOrderPlace(WorkflowTransitionEvent $event) {
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $event->getEntity();
// Call our service to export the order.
$exported = $this->orderExporter->export($order);
// Update the success flag on the order of the results of the export.
$order->setData('export_success', $exported);
}
}
Nothing scary here, we are just subscribing to the ‘commerce_order.place.post_transition’ event and defining a function to run in reaction to that event (onOrderPlace). Each commerce order transition defined in your order workflow automatically has a ‘pre_transition’ and ‘post_transition’ event made available, and we are subscribing to the post_transition event for the place transition. In this example, the place transition is the specific transition that happens when the order has been successfully paid for.
Inside our onOrderPlace() function, we get the order entity that is available in this event, calling the custom service that we have injected into the class, to do the exporting of the order. We then set a flag on the order using the setData method (which lets us set arbitrary data against the order) with the result of the order export from our custom service.
Of course, nothing is stopping you from having code inside of this onOrderPlace() function that does the actual exporting but we usually like to separate the logic into its own service class. This separation approach means that we can then easily call the order exporting service in other places that we might want it, such as a Drush command.
We’ll also need to let Drupal know about our new event subscriber so will need to add an entry into our module’s services.yml file, e.g.
services:
example_order_exporter.order_export_subscriber:
class: Drupal\example_order_exporter\EventSubscriber\OrderExporterSubscriber
arguments: ['@example_order_exporter.order_exporter']
tags:
- { name: event_subscriber }
In this example, we have also specified our custom order exporter service as an argument to our order subscriber (that would also have a definition in this .yml file). If you have decided to include all of the order export logic directly inside the onOrderPlace() function (or aren’t using a custom service to do such a thing) then you can omit this from the arguments to the subscriber and from the __construct() method of the subscriber in the OrderExporterSubscriber.php file.
Step 2 - The hook_commerce_order_update() implementation
The second part of the work here is to have a hook_commerce_order_update implementation inside of our custom module that will handle checking the order data that we set previously in our subscriber, and then apply the appropriate transition if successful.
It’s important that we do this here in the hook implementation! If we try to apply the transition directly inside of the onOrderPlace function from our event subscriber, then this would start the transitioning process for our new transition before the other transition has fully finished. This means that the original transition wouldn’t have necessarily been completed and any functionality driven from the transition we just applied would run, and then the original state transition that we subscribed to would try and finish off afterwards.
Aside from being logically incorrect, this leads to weird inconsistencies in the order log where you end up with something like this, e.g.
"Order state was moved from Draft to Exported"
"Order state was moved from Exported to Exported"
Instead of what it should be:
"Order state was moved from Draft to Placed"
"Order state was moved from Placed to Exported"
Here’s the sample code that would need to go into the .module file of the example_order_exporter module (example_order_exporter.module).
use Drupal\commerce_order\Entity\OrderInterface;
/**
* Implements hook_commerce_order_update().
*/
function example_order_exporter_commerce_order_update(OrderInterface $order) {
// Check that the current state is 'payment' (i.e. we have finished
// transitioning to the payment state) and that the export_success flag is
// set to TRUE on the order, which indicates our event subscriber that exports
// the order has successfully run.
// This hook is called *after* the transition has finished, so we can safely
// apply the new transition to 'completed' here.
$order_state = $order->getState();
$current_state = $order_state->getId();
if ($current_state === 'payment') {
if ($order->getData('export_success') === TRUE) {
$order_state_transitions = $order_state->getTransitions();
$transition_to_apply = 'completed';
// Update the state of the order by applying the transition.
// If the order state transitions are empty then this order is
// 'completed' and shouldn't have its state changed.
if (isset($order_state_transitions[$transition_to_apply])) {
$order_state->applyTransition($order_state_transitions[$transition_to_apply]);
$order->save();
}
}
}
}
We check the current state of the order is what we expect it to be - payment - and if the export_success flag we set previously is true. Only if these two conditions are true do we then try to apply the transition with the useful applyTransition method on the order state.
The $order_state has a useful method called getTransitions which returns an array of all the valid transitions for the order in its current state. This allows us to do a quick check to be sure that the ‘completed’ state (the final state of our order workflow) is present in the list of allowed transitions, before trying to apply it. If not present, this would mean this order has already been exported, and we don't try to export the order again.
We are being slightly sneaky here by calling a save on the order at this point in time, as this commerce_order_update hook is triggered during the postSave hook of the original order save. To be safe, we’ll use the (always handy) hook_module_implements_alter to ensure that our hook implementation here always runs last. This will ensure we aren’t accidentally stopping any other module’s hook_commerce_order_update implementation from running before ours.
/**
* Implements hook_module_implements_alter().
*/
function example_order_exporter_module_implements_alter(&$implementations, $hook) {
if ($hook == 'commerce_order_update') {
$group = $implementations['example_order_exporter'];
unset($implementations['example_order_exporter']);
$implementations['example_order_exporter'] = $group;
}
}
So there you have it, a nice clean way of having an automated transition run on the order without getting into any weird state transition issues caused by calling applyTransition whilst reacting to a previous transition. In this example, it’s used purely when we are exporting an order after payment has been made, but nothing is stopping you from reacting to other order transitions depending on your workflow’s needs!
LN Webworks: The 8-Step Anatomy of a Successful UX Design Process
Opensource.com: Open source text editing for your website with CKEditor
Use the power of JavaScript and CKEditor to bring rich text editing to your website.
Most applications allow users to create and add some textual content, such as a comment, a chat message, an article, a product description, or a legal document. Today, plain…
DevCollaborative: Choosing the Right Analytics Tool For Your Nonprofit Website
Chromatic Insights: Drupal 7 End-of-Life Podcast - Episode 02
Drupal Association blog: DrupalCon Health and Safety Policies in a Changing World
We are looking forward to gathering in person at DrupalCon Pittsburgh!
And current projections show that many will be gathering: registrations are up 32% over last year.
There is much work being done to make DrupalCon Pittsburgh the ultimate Drupal event in North America, one that generates excitement for Drupal and spurs the creativity that abounds within the Drupal Community.
We also recognize that we’re doing this planning work amid rapidly changing pandemic conditions. The Drupal Association has been monitoring COVID-19 trends and has seen a marked decrease in Covid-19 cases both nationally and in Pittsburgh. The CDC reports that weekly cases since January for the U.S. have dropped 67%, and for Allegheny County (which incorporates Pittsburgh), it has dropped 60%. Pittsburgh is currently rated as “low risk” by the CDC. This assessment is matched by the World Health Organization’s trends, which show a significant decline in cases in the U.S.
This is GREAT news! News that I hope makes everyone feel more comfortable in joining us in Pittsburgh.
Some have been asking if that means we are going to change our health and safety policies for this event.
In January, we updated our health and safety policies prior to opening registration. This update removed a vaccine or daily testing requirement, but retained masking indoors. Let me explain why.
The COVID-19 pandemic forced the Drupal Association to implement extraordinary measures to protect our community. At the same time, we knew the situation would constantly evolve, and our response would also have to evolve. We knew we would have to closely monitor medical, scientific, and public health data and make appropriate changes to the DrupalCon Health and Safety Policies.
We decided early on that we needed a decision rooted in our values: DrupalCon should be a safe, accessible, and inclusive space for everyone in our community. It is often the most vulnerable in our community who are left behind, and so we felt it important to ensure our criteria for policy changes centered on those most at risk.
With that value at heart, we looked at the following criteria:
- Public health metrics and recommendations from International and National organizations such as the WHO and CDC, including whether these organizations have updated their designation from pandemic to endemic.
- Medical/Scientific studies being released by reputable, peer-reviewed sources.
- Comparable industry events whose values align with our own, especially other Drupal community events.
- Pragmatic and logistical constraints for us and attendees in event planning.
This last bullet is worth a bit more explanation. The Association realized a decision was needed prior to opening registration for each event and then to stick to the decision. Attendees register with the expectations presented at the time of registration and changing after the fact can make some participants feel like the rules are changing against them unfairly and cause them to question our reasoning. We have observed other conferences that changed health & safety policies mid-registration with significant consequences.. Our default position is to change policies between events’ registration periods, but not during.
So how have we used those criteria with our values so far?
In 2020, with no vaccines approved and clear national guidance and legal mandates, it was clear that both DrupalCon North America and Europe would have to become virtual events. Both were held successfully, and we even had an influx of first time attendees who would not otherwise have been able to attend.
In 2021, although the first vaccines were now available under an emergency use authorization, they would not receive full FDA approval until August of that year. At the same time, the pattern of multiple infection waves and new variants had become apparent, and vaccination rates and hospitalization levels were still very high. Both DrupalCon events would again be virtual in 2021.
In 2022, most national and regional public health recommendations began to allow for in-person events again. After a review of Oregon and Multnomah county public health mandates and metrics, it was decided that a Vaccine-or-daily-test requirement and masking was required. Metrics in Europe improved more rapidly than those in North America, and thus DrupalCon Prague did not require vaccination but did include a mask requirement.
So where does that leave us in 2023?
We are monitoring the trends but have not changed our health and safety policy for DrupalCon Pittsburgh.
We are following our default position of maintaining policies in place at registration opening. COVID-19 is still identified as a pandemic, with new cases, hospitalizations, and deaths still occurring.
But monitoring is appropriate, as is evolving. If the recent very positive trends continue, the health numbers for Covid prevalence in Pittsburgh may be very low by June. Consequently, some attendees may be frustrated by these continued safety measures and this may affect compliance at the event. A lack of compliance undermines the health benefits of our health and safety policies.
Thus, community feedback is crucial.
I invite feedback from the Drupal community on this blog post and our health and safety policies for DrupalCon Pittsburgh. How we can create an inviting and welcoming atmosphere for all community members amidst a changing world of health metrics.
If you’re so inclined, please share your thoughts with us.
DrupalCon Lille 2023
This month has been the same time window where we have to utilize these decision making criteria to set our policies for DrupalCon Lille.
- The World Health Organization (WHO) and French national guidelines recommend but do not require masking in any setting, including hospitals and nursing homes, though it is still recommended in gatherings with vulnerable people. Vaccination and testing requirements for entry to the country have not been required for about a year.
- The entire country of France has a rate of hospital admissions and covid-related deaths comparable to just Alleghany County(where Pittsburgh is located) in the USA. The per-capita daily mortality rate due to covid in all of France is less than 10% the rate in Alleghany County.
- Regional events in Europe have almost entirely eliminated their extraordinary covid measures.
Therefore, we anticipate that DrupalCon Lille will not be requiring proof of vaccination, daily testing, or masking (though masking is still recommended).
We hope that this retrospective and more detailed explanation of our DrupalCon health and safety policy decision criteria is helpful.
I look forward to gathering with the Drupal community in Pittsburgh, meeting as many people as possible, and hearing your ideas about how we can advance Drupal.
Tim Doyle
CEO
Drupal Association
LN Webworks: 5 Unique Startup Ideas to Leverage the Power of GPT-4 and Drupal
Lullabot: Guardrails and Content Authoring Flexibility: Finding the Right Balance
Content teams want the flexibility to publish content creatively. They want landing pages to be dynamic and reflect the vision they have inside their heads. Organizations want to encourage brand and writing-style consistency (sometimes across a whole network of websites). They also want to ensure their content is maintainable and meets web accessibility and security standards.