Implementing a Checkout Flow resolver in Drupal Commerce 2.0

Published onTuesday 10, October 2017

This is a new format I'm trying which is more tutorial based. Let me know what you think! I plan on more regular content like this.

CheckoutResolverInterface.php

What are we going to learn?

Drupal Commerce 2 allows you to support multiple checkout forms. This is pretty cool because it allows you to have different checkout experiences for your customers based on what they are purchasing. We're going to dive into why you would want to use a checkout flow resolver and how to implement one.

  • Understand the order and checkout relationship
  • Implement a checkout flow resolver service

Getting started

We are going to create a checkout flow resolver which inspects the customer's order. The goal of our checkout flow resolver is to see if the order contains both physical and digital goods. If the order contains both types of products, we're going to provide a checkout flow plugin that should be used. This will then be the checkout flow plugin used, ensuring our customer has the correct checkout experience.

By default, a checkout flow uses the one configured on the order type. By following this tutorial you could rely on a single order type instead of multiple order types (one for physical, one for digital.)

To keep the tutorial short, we are going to assume there are the following checkout flows configured:

  • Digital (machine name: digital)
  • Shipping (machine name: shipping)
  • Mixed order (machine name: mixed_order)

You should also have a custom module, which we will refer to as mymodule.

Determining an order's checkout flow

The checkout module adds a setting to order types. This setting specifies what checkout flow the order will use. This setting is the default value used and can be overridden through checkout flow resolvers. The concept of resolvers is used quite often in Drupal Commerce. It is a design pattern which uses a "first come first serve" basis. Once a resolver returns a value, that value is used.

In the case for checkout flows, this is performed by the commerce_checkout.chain_checkout_flow_resolver service. It looks for services tagged as commerce_checkout.checkout_flow_resolver. If you aren't familiar with tagged services, that is fine. There's an example below. Or you can learn more about them on the Drupal.org Service Tags documentation.

The Commerce Checkout module provides a checkout flow resolver, commerce_checkout.default_checkout_flow_resolver. This is the default resolver. It's intended to run last and will provide the default value for a checkout flow. The one on the order type's setting.

An order's checkout flow is discovered through the commerce_checkout.checkout_order_manager service. When an order enters the checkout process that service is invoked. It then looks up the order's checkout flow through the chain resolver.

Creating the resolver

Our first step is to create the resolver. In your module's directory, create an src/Resolvers directory. Nothing requires resolvers go to into a Resolver namespace, but it's just more organized. Then create a CheckoutFlowResolver.php in that directory.

Inside of our CheckoutFlowResolver.php file let's sub out our class.

<?php

namespace Drupal\mymodule\Resolvers;

use Drupal\commerce_checkout\Resolver\CheckoutFlowResolverInterface;

class CheckoutFlowResolver implements CheckoutFlowResolverInterface {

}

We need to satisfy the requirements of the CheckoutFlowResolverInterface interface. To do so, we implement a the resolve method which accepts an order object.

<?php

namespace Drupal\mymodule\Resolvers;

use Drupal\commerce_checkout\Resolver\CheckoutFlowResolverInterface;
use Drupal\commerce_order\Entity\OrderInterface;

class CheckoutFlowResolver implements CheckoutFlowResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(OrderInterface $order) {
    // TODO: Implement resolve() method.
  }

}

Inside of our resolve method, we can implement any logic necessary to determine the checkout flow that should be used. If we do not choose to return a checkout flow, the resolving logic will move onto the next resolver. By default, this will then fallback to the checkout flow configured on the order type itself.

Let's assume our site has a digital order type which uses the digital checkout flow. There is also the default order type which uses the shipping checkout flow. In our example, we want to provide a way for mixed orders to use a different checkout flow called mixed_order.

We'll update our checkout flow resolver to check the order's bundle. If a digital order has shippable products or a shippable order has a digital product we will return a checkout flow and override the default.

<?php

namespace Drupal\mymodule\Resolvers;

use Drupal\commerce_checkout\Entity\CheckoutFlow;
use Drupal\commerce_checkout\Resolver\CheckoutFlowResolverInterface;
use Drupal\commerce_order\Entity\OrderInterface;

class CheckoutFlowResolver implements CheckoutFlowResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(OrderInterface $order) {
    if ($order->bundle() == 'digital') {
      foreach ($order->getItems() as $item) {
        // Shippable items have a weight field.
        if ($item->hasField('weight')) {
          return CheckoutFlow::load('mixed_order');
        }
      }
    }
    else {
      // Check if the default order has digital items.
      foreach ($order->getItems() as $item) {
        if (!$item->hasField('weight')) {
          return CheckoutFlow::load('mixed_order');
        }
      }
    }
  }

}

Registering our resolver

We have created the resolver. Drupal is not aware of it, yet. We must create a mymodule.services.yml which registers the class to Drupal's service container. We will then tag it as a checkout flow resolver.

services:
  mymodule.checkout_flow_resolver:
    class: Drupal\mymodule\Resolvers\CheckoutFlowResolver
    tags:
      - { name: commerce_checkout.checkout_flow_resolver, priority: 100 }

The commerce_checkout.services.yml file defines the service which collects all services using the commerce_checkout.checkout_flow_resolver tags. These are then run in priority (100 is greater than -100.) The default checkout flow resolver, which reads from the order type's setting, runs at priority -100.

NOTE! Whenever you make adjustments to a module's services.yml you must rebuild Drupal's caches.