Working with the Drupal Commerce 2 checkout flows

Tunnel

Drupal Commerce 2 allows to define out of the box multiple checkout flows, allowing to customize according to the order, the product purchased, the customer profile this buying process and modify it accordingly. This is an extremely interesting feature, in that it can simplify as much as necessary this famous checkout flows. Do you sell physical (and therefore with associated delivery) and digital (without delivery) products? In a few clicks you can have two separate checkout flows that will take into account these specificities.

On the other hand, during my first contact with Drupal Commerce 2, I was somewhat taken aback by the fact that an order type could only be associated with one and only one checkout flow. And that a product type could only be associated with one and only one order type. As a result, one product type could only be associated with one and only one checkout flow. Diantre! But how to make products that can have physical and digital variations (like books for example, with a paper version and a digital version) ?

Sélection d'un tunnel d'achat sur le type de commande

Which checkout flow to associate with an order type that can correspond to physical or digital products? Should we multiply the order types accordingly? What impact on the catalog architecture?

As you have understood, this has raised many questions.

A default purchase tunnel

Fortunately, I misinterpreted this setting of the checkout flow on the order type. After analysis, it became clear to me that it was not a question here of setting up a single checkout flow for an order type, but to define the checkout flow that would be used by default for this order type.

Indeed, as for the prices, the taxes, the order elements, the shop, etc. Drupal Commerce 2 uses the Resolver concept to determine which checkout flow to use. And because of this, Drupal Commerce 2 makes it possible to address multiple needs very easily, while offering a standard operation as soon as it is installed.

Thus, the determination of a checkout flow to be used for an order is made during the first entry of a cart (or a draft order) in the checkout flow.

/**
 * {@inheritdoc}
 */
public function getCheckoutFlow(OrderInterface $order) {
  if ($order->get('checkout_flow')->isEmpty()) {
    $checkout_flow = $this->chainCheckoutFlowResolver->resolve($order);
    $order->set('checkout_flow', $checkout_flow);
    $order->save();
  }

  return $order->get('checkout_flow')->entity;
}

Indeed, as can be seen, when entering into the checkout flow, if the order does not yet have an associated checkout flow, then it is called the Resolver chainCheckoutFlowResolver->resolve() which then determines which checkout flow to use and then saves it on the order.

Changing a checkout flow for a given order then becomes child's play.

A dynamic checkout flow

To determine in a very granular way which checkout flow to use for each order, it is then enough to carry out two operations.

  • When adding or deleting a product to the cart, simply reset the checkout flow associated with the order (because as we have seen, the latter is determined only if it has not already been associated and registered with an order)
  • Create a service that will implement the Resolver commerce_checkout.checkout_flow_resolver, with a higher priority than the service responsible for determining the default checkout flow (set to -100)

Let's move on to practice.

In our module named MY_MODULE, let's create a EventSubscriber service that will subscribe to add and delete products to a command to reset the purchase tunnel on the order.

In the directory of our module, we create the file my_module.services.yml and declare our service my_module.cart_update_subsciber.

services:

  my_module.cart_update_subscriber:
    class: Drupal\my_module\EventSubscriber\CartEventSubscriber
    arguments: ['@entity_type.manager']
    tags:
      - { name: event_subscriber }

  my_module.checkout_flow_resolver:
    class: Drupal\my_module\Resolver\CheckoutFlowResolver
    tags:
      - { name: commerce_checkout.checkout_flow_resolver, priority: 100 }

 

We also take this opportunity to create our my_module.checkout_flow_resolver service which will dynamically determine which checkout flow to use.

The first step is to ensure that the checkout flow associated with an order is reset when adding or deleting a product from the order with our Class CartEventSubscriber.

<?php

namespace Drupal\my_module\EventSubscriber;

use Drupal\commerce_cart\Event\CartEntityAddEvent;
use Drupal\commerce_cart\Event\CartEvents;
use Drupal\commerce_cart\Event\CartOrderItemRemoveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CartEventSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [
      CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', -50],
      CartEvents::CART_ORDER_ITEM_REMOVE => ['onCartOrderItemRemove', -50],
    ];
    return $events;
  }

  /**
   * Resets the checkout flow status when an item is added to the cart.
   *
   * @param \Drupal\commerce_cart\Event\CartEntityAddEvent $event
   *   The cart event.
   */
  public function onCartEntityAdd(CartEntityAddEvent $event) {
    $cart = $event->getCart();
    if ($cart->hasField('checkout_flow')) {
      $cart->set('checkout_flow', NULL);
    }
  }

  /**
   * Resets the checkout flow status when an item is removed from the cart.
   *
   * @param \Drupal\commerce_cart\Event\CartOrderItemRemoveEvent $event
   *   The cart event.
   */
  public function onCartOrderItemRemove(CartOrderItemRemoveEvent $event) {
    $cart = $event->getCart();
    if ($cart->hasField('checkout_flow')) {
      $cart->set('checkout_flow', NULL);
    }
  
    // If an order item is removed, this can be the only shippable item in the
    // cart. So we reset the shipmemnt order when on order item is removed. The
    // customer could already go to the checkout and get a shipment. And if so
    // then he can have a free order but with a shipment and so a shipping amount.
    /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface[] $shipments */
    $shipments = $cart->get('shipments')->referencedEntities();
    foreach ($shipments as $shipment) {
      $shipment->delete();
    }
    $cart->set('shipments', NULL);
  }

}

Note here that in addition to the need to reset the checkout flow during these two events, we also delete any shipment item that could have been associated with an order. Indeed, in this example, if we want to determine different checkout flows depending on whether or not the order requires a shipment (and associated fees), we must ensure that if an order had shipment items, those they are recalculated if necessary in the checkout flow (the typical case is an order which have a physical product to be delivered, which would have gone through the checkout flow for the first time without reaching its conclusion, and for which the customer changes his mind to return to the cart and delete this physical product, thereby making his order without further shipment necessary).

Once this step has been completed, all that remains is to create our Resolver, which can dynamically determine which checkout flow to use.

<?php

namespace Drupal\my_module\Resolver;

use Drupal\my_module\MyModuleInterface;
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) {

    // Free product flag.
    $free = TRUE;
    // Immaterial product.
    $numerical = TRUE;

    if (!$order->getTotalPrice()->isZero()) {
      $free= FALSE;
    }

    // Have we a shippable product in the order ?
    foreach ($order->getItems() as $item) {
      $purchased_entity = $item->getPurchasedEntity();
      if ($purchased_entity->hasField(MyModuleInterface::WEIGHT_FIELD_NAME)) {
        if (!$purchased_entity->get(MyModuleInterface::WEIGHT_FIELD_NAME)->isEmpty()) {
          /** @var \Drupal\physical\Measurement $weight */
          $weight = $purchased_entity->get(MyModuleInterface::WEIGHT_FIELD_NAME)->first()->toMeasurement();
          if (!$weight->isZero()) {
            $numerical = FALSE;
            break;
          }
        }
      }
    }

    // If we only have product free and without weight. So go to the
    // direct checkout flow.
    if ($free && $numerical) {
      return CheckoutFlow::load('direct');
    }
    // If numerical product but non free, go to to the download checkout
    elseif (!$free && $numerical) {
      return CheckoutFlow::load('download');
    }

  }

}

Here, in this example, we determine if the order contains free products and / or containing a (non-empty) weight which therefore requires shipment. You can of course customize as much as you need, depending on your project, the determination of the checkout flow. The possibilities are endless, at your fingertips.

Thus in a few lines, we have just defined three types of checkout flows that can be used in a granular way for each order according to the elements of this order. Note here that we do not determine the checkout flow to use unless the order contains only digital and/or free products. In the opposite case, then we leave the hand to the default Resolver which will determine the checkout flow compared to the setting made on the order type configuration.

This example shows us how it can be easy, with the help of a Drupal 8 developer, to set up specific process with Drupal Commerce 2.

A modular design of Drupal Commerce 2

This introduction to checkout flows, finally, could be duplicated to many configuration elements of a Drupal Commerce 2 website. The configuration of a standard catalog, from the products, orders to order's items, the determination of the product price, a shop (in the case of a marketplace with multiple shops) is actually not a fixed configuration in the marble, but a configuration of the default behavior for an online store. This behavior can then be altered according to business needs in a simple and robust way.

An undeniable asset when it comes to setting up an e-commerce site with the logic and needs of the most classic to the most unusual.

 

Commentaires

Soumis par Vincent Lalieu (non vérifié) le 16/11/2019 à 12:57 - Permalien

Whatever I do in trying to implement your solution , I get this error message when installing the module:
Service 'myservice.checkout_flow_resolver' for consumer 'commerce_checkout.chain_checkout_flow_resolver' does not implement Drupal\commerce_checkout\Resolver\CheckoutFlowResolverInterface.
maybe I am missing something.

Ajouter un commentaire