Commerce 2.x Stories: Workflows
Now that we’ve covered products, it’s time to jump into orders. We are improving many aspects of orders based on previous feedback. One such aspect is the concept of order statuses and states.
In Commerce 1.x orders have a status. The status indicates the current checkout page, whether the order is a cart, whether it has been paid and fulfilled (shipped), or maybe canceled/refunded. Statuses are sequential, and one goes after another. They are grouped by states, e.g. all checkout statuses belong to the "checkout" state.
The problem with this model is that one list of statuses indicates multiple concepts (checkout state, payment state, fulfillment state, etc). These concepts are parallel and trying to handle them sequentially creates bugs and confusion. For example, an order might be paid before or after checkout completes due to the async nature of certain payment gateways, or because the business is invoicing clients at the end of the month. Furthermore, the system doesn't enforce the requirement for the status to change sequentially; it can be set to any other status at any point. There is also no way to express rules such as “only completed orders can be refunded“ or “completed orders can’t be sent back to checkout”.
Our requirements for Commerce 2.x were:
- Remove the hazy distinction between status and state. Have only states.
- Split the order workflow into multiple workflows (order, checkout, payment, fulfillment) plus a boolean field indicating “is this order a cart“,
- Allow different order types to use different workflows (a t-shirt might go through different states than a DrupalCon ticket).
- Provide an API for expressing allowed transitions between states, allowing for better UIs and validation.
To solve this on an API level, we introduced the concept of a workflow, which is a collection of states (id, label) and transitions (id, "from" states, "to" state, label). We also created workflow groups, a way to group workflows used for the same purpose (order workflows, payment workflows, product marketing workflows, etc.). Workflows and groups would be defined in hooks in D7. Since this is D8, they are defined in yaml, just like menu links.
mymodule.workflow_groups.yml:order:
label: Order
entity_type: commerce_order
default:
id: default
label: Default
group: order
states:
new:
label: New
fulfillment:
label: Fulfilment
completed:
label: Completed
canceled:
label: Canceled
transitions:
create:
label: Create
from: [new]
to: fulfillment
fulfill:
label: Fulfill
from: [fulfillment]
to: completed
cancel:
label: Cancel
from: [new, validation, fulfillment]
to: canceled
These examples are shortened for brevity, the real order workflow will have additional states and transitions. Transitions can further be limited by guard classes such as this one.
The current state is stored in a special field type (StateItem), which references the used workflow, and acts as a state machine. It has an API for getting the allowed transitions:
<?php
$order_state = $order->getState();
print $order_state->value; // fulfillment
// Get the allowed transitions for the current state.
$transitions = $order_state->getTransitions();
// All transitions have a translatable label that can be shown in the UI (great for action buttons)
print_r($transitions['completed']->getLabel());
// Same as $order_state->value = 'completed';
$order_state->applyTransition($transitions['complete']);
?>
A matching validator is provided that ensures a valid state was set, taking into account the previous value as well.
Putting it together
Since these APIs aren't Commerce specific, we have placed them into a newly created State Machine module. The README provides a more detailed overview of the offered functionality. As of this morning, the code is also up on drupal.org, with a beta release planned for next week.
With the help of this module, the future looks like this:
These are the default workflows, designed to be generic, and overridable per order type. Of course, their states and transitions might change over the future pre-releases. The most interesting workflow here is the order workflow. Let's look at the available states:
- new: Not been placed yet (in checkout or being edited by the admin)
- validation: Aawaiting validation (reviewing a fraud score, email verification by the customer, etc)
- fulfillment: Awaiting fulfilment (sending all of the relevant packages)
- completed: Completed and no longer changeable.
In conclusion, we've dramatically improved a very visible developer API. We also left room for a possible UI in the future (in contrib). Next week we'll look at carts and order / line item types.
Comments
print_r($transitions['completed']->getLabel());
Is this line of code guaranteed to work? Meaning that it's impossible to removed the completed state?
Even if that's the case it would be nice to use a "magic string" (completed) to get the completed state.
And if it is impossible to remove the completed state couldn't we have a fluid syntax to do this. that way the implementation wouldn't be dependent on the keys never changing.
The code is referencing a
The code is referencing a transition, not a state. Workflows are not alterable, so if it's in the yaml file, we know it's also in the array.
I'm unsure what kind of fluid syntax you have in mind. Perhaps you can open an issue in the state_machine issue queue for us to discuss?