Utiliser les tunnels d'achat de Drupal Commerce 2

Un tunnel en bois

Drupal Commerce 2 permet de définir out of the box de multiples tunnels d'achats, permettant de personnaliser selon la commande, le produit acheté, le profil client ce processus d'achat et de le modifier en conséquence. C'est une fonctionnalité extrêmement intéressante, en cela qu'elle peut permettre de simplifier autant que de besoin ce fameux tunnel d'achat. Vous vendez des produits physiques (et donc avec une livraison associée) et numériques (sans besoin de livraison) ? En quelques clics vous pouvez disposer de deux tunnels d'achats distincts qui tiendront compte de ces spécificités.

En revanche, lors de mon premier contact avec Drupal Commerce 2, j'ai été quelque peu interloqué par le fait qu'un type de commande ne pouvait être associé qu'à un et seul tunnel d'achat. Et qu'un type de produit ne pouvait être associé qu'à un et seul type de commande. Et par conséquent, un type de produit ne pouvait être associé qu'à un et un seul tunnel d'achat. Diantre ! Mais comment faire pour des produits qui peuvent avoir des variations physiques et numériques (comme les livres par exemples, avec une version papier et une version numérique).

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

Quel tunnel d'achat associer à un type de commande qui peut correspondre à des produits physiques ou numériques ? Faut-il démultiplier les types de commandes en conséquence ? Quel impact sur l'architecture de mon catalogue ?

Vous l'avez compris, cela a suscité beaucoup d'interrogations.

Un tunnel d'achat par défaut

Heureusement, j'avais mal interprété ce paramétrage du tunnel d'achat sur les types de commandes. Après analyse, il m'est devenu évident qu'il ne s'agissait pas ici de paramétrer un seul et unique tunnel d'achat pour un type de commande, mais de définir le tunnel d'achat qui serait utilisé par défaut pour ce type de commande.

En effet, comme pour les prix, les taxes, les éléments de commande, la boutique elle-même, etc. Drupal Commerce 2 recourt au concept du Resolver pour déterminer le tunnel d'achat à utiliser. Et de ce fait, Drupal Commerce 2 permet d'adresser de multiples besoins très facilement, tout en proposant un fonctionnement standard dès son installation.

Ainsi, la détermination d'un tunnel d'achat à utiliser pour une commande est réalisée lors de la première entrée d'un panier (ou d'une commande au statut brouillon) dans le tunnel d'achat.

/**
 * {@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;
}

En effet, comme on peut le voir, lors d'une entrée dans le tunnel d'achat d'une commande, si celle-ci ne dispose pas encore d'un tunnel associée, alors il est fait appel au Resolver chainCheckoutFlowResolver->resolve() qui détermine alors le tunnel d'achat à utiliser et l'enregistre alors sur la commande.

La modification d'un tunnel d'achat pour une commande donnée devient alors un jeu d'enfant.

Un tunnel d'achat dynamique

Pour déterminer de façon très granulaire quel tunnel d'achat utiliser pour chaque commande, il suffit alors de réaliser deux opérations.

  • Lors de l'ajout ou de la suppression d'un produit au panier, il suffit de réinitialiser le tunnel d'achat associée à la commande (car comme nous l'avons vu, ce dernier n'est déterminé que s'il n'a pas déjà été associé et enregistré avec une commande)
  • Créer un service qui va implémenter le Resolver commerce_checkout.checkout_flow_resolver, avec une priorité supérieure au service chargé de déterminer le tunnel d'achat par défaut (défini à -100)  

Passons à la pratique.

Dans notre module intitulé MY_MODULE, créons un service de type EventSubscriber qui va souscrire aux événements d'ajout et de suppression de produits à une commande pour réinitialiser le tunnel d'achat sur la commande.

Dans le répertoire de notre module, nous créons le fichier my_module.services.yml et y déclarons notre 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 }

 

Nous en profitons également pour créer notre service my_module.checkout_flow_resolver qui sera chargé de déterminer dynamiquement quel tunnel d'achat utiliser.

Le première étape consiste donc à s'assurer que le tunnel d'achat associé à une commande soit réinitialisé lors de l'ajout ou la suppression d'un produit à une commande avec notre 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);
  }

}

Notons ici que outre le besoin de réinitialiser le tunnel d'achat lors de ces deux événements, nous supprimons également tout élément de livraison qui aurait pu être associé à une commande. En effet, dans cet exemple, si nous souhaitons déterminer différents tunnels d'achats selon que la commande nécessite ou non une livraison (et des frais associés), il nous faut nous assurer que si une commande disposait d'éléments de livraison, ceux-ci soient recalculés le cas échéant dans le tunnel d'achat (le cas typique est celui d'une commande comportant un produit physique à livrer, qui aurait traversé une première fois le tunnel d 'achat sans toutefois aller à sa conclusion, et pour lequel le client change d'avis pour revenir au panier et supprimer ce produit physique, rendant de fait sa commande sans plus de livraison nécessaire).

Une fois cette étape réalisée il ne reste plus qu'à créer notre Resolver qui pourra déterminer dynamiquement quel tunnel d'achat utiliser.

<?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();
          $is_zero = $weight->isZero();
          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');
    }

  }

}

Ici dans cet exemple, nous déterminons si la commande contient des produits gratuit et/ou contenant un poids (non vide) qui de ce fait nécessite une livraison. Vous pouvez bien sûr personnaliser autant que de besoin, selon votre projet, la détermination du tunnel d'achat. Les possibilités sont infinies, à portée de main.

Ainsi en quelques lignes, nous venons de définir trois types de tunnels d'achats qui pourront être utilisés de façon granulaire pour chaque commande en fonction des éléments de cette commande. Notons ici que nous ne déterminons nous-même le tunnel d'achat à utiliser que si la commande ne contient que des produits numériques et/ou gratuits. Dans le cas contraire, alors nous laissons la main au Resolver par défaut qui déterminera le tunnel d'achat par rapport au paramétrage réalisé sur le type de commande.

Cet exemple nous montre comment il peut être aisé, avec l'aide d'un développeur Drupal 8, de mettre en place des logiques spécifiques avec Drupal Commerce 2.

Une conception modulaire de Drupal Commerce 2

Cette introduction aux tunnels d'achats, finalement, pourrait être dupliquée à de nombreux éléments de configuration d'un site Drupal commerce 2. La configuration d'un catalogue type, depuis les produits, les commandes ou les éléments de commande, la détermination du prix du produit, d'une boutique (dans le cas d'une place de marché avec de multiples boutiques) concerne en fait non pas une configuration figée dans le marbre, mais une configuration du comportement de la boutique en ligne par défaut. Ce comportement peut alors être altéré selon les besoins métiers de façon simple et robuste.

Un atout indéniable quand il s'agit de mettre en place un site e-commerce aux logiques et aux besoins des plus classiques aux plus atypiques.

 

Commentaires

Soumis par Stève (non vérifié) le 11/05/2018 à 20:26 - Permalien

J'ai des produits livrables mais j'aimerai pouvoir laisser le choix au client de pouvoir récupérer sa commande sur place, donc sans "shipping". Il faudrait pouvoir passer d'un tunnel a l'autre une fois dans un tunnel (là ou le client choisi ou non une livraison)...
As-tu une idée de comment faire ça ?
Merci.

Ajouter un commentaire