Swapping Drupal 8 services to customize Drupal Commerce

Published Jul 24, 2017

One of the reasons that I love Drupal 8 is the fact it is object orientated and uses the Dependency Injection pattern with a centralized service container. If you’re new to the concept, here’s some links for some fun reading.

But for now the basics are: Things define their dependencies, and a centralized thing is able to give you an object instance with all of those dependencies provided. You don’t need to manually construct a class and provide its dependencies (constructor arguments.)

This also means we do not have to use concrete classes! That means you can modify the class used for a service without ripping apart other code. Yay for being decoupled(ish)!

Why is this cool?

So that’s great, and all. But let’s actually use a real example to show how AWESOME this is. In Drupal Commerce we have the commerce_cart.cart_session service. This is how we know if an anonymous user has a cart or not. We assume this service will implement the \Drupal\commerce_cart\CartSessionInterface interface, which means we don’t care how you tell us, just tell us via our agreed methods.

The default class uses the native session handling. But we’re going to swap that out and use cookies instead. Why? Because skipping the session will preserve page cache while browsing the site catalogs and product pages.

Let’s do it

Let’s kick it off by creating a module called commerce_cart_cookies. This will swap out the existing commerce_cart.cart_session service to use our own implementation which relies on cookies instead of the PHP session.

The obvious: we need a commerce_cart_cookies.info.yml

    name: Commerce Cart Cookies
    description: Uses cookies for cart session instead of PHP sessions
    core: 8.x
    type: module
    dependencies:
    - commerce_cart

Now we need to create our class which will replace the default session handling. I’m not going to go into what the entire code would look like to satisfy the class, but the generic class would resemble the following. You can find a repo for this project at the end of the article.

<?php

namespace Drupal\commerce_cart_cookies;

use Drupal\commerce_cart\CartSessionInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Uses cookies to track active carts.
 *
 * We inject the request stack to handle cookies within the Request object,
 * and not directly.
 */
class CookieCartSession implements CartSessionInterface {

  /**
   * The current request.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * Creates a new CookieCartSession object.
   *
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   */
  public function __construct(RequestStack $request_stack) {
    $this->request = $request_stack->getCurrentRequest();
    }

    /**
    * {@inheritdoc}
    */
    public function getCartIds($type = self::ACTIVE) {
    // TODO: Implement getCartIds() method.
    }

    /**
    * {@inheritdoc}
    */
    public function addCartId($cart_id, $type = self::ACTIVE) {
    }

    /**
    * {@inheritdoc}
    */
    public function hasCartId($cart_id, $type = self::ACTIVE) {
    // TODO: Implement hasCartId() method.
    }

    /**
    * {@inheritdoc}
    */
    public function deleteCartId($cart_id, $type = self::ACTIVE) {
    // TODO: Implement hasCartId() method.
    }

}

Next we’re going to make our service provider class. This is a bit magical, as we do not actually register it anywhere. It just needs to exist. Drupal will look for classes that end in ServiceProvider within all enabled modules. Based on the implementation you can add or alter services registered in the service container when it is being compiled (which is why the process is called rebuild! not just cache clear in Drupal 8.) The class must also start with a camel cased version of your module name. So our class will be CommerceCartCookiesServiceProvider.

Create a src directory in your module and a CommerceCartCookiesServiceProvider.php file within it. Let’s scaffold out the bare minimum for our class.

<?php

namespace Drupal\commerce_cart_cookies;

use Drupal\Core\DependencyInjection\ServiceProviderBase;

class CommerceCartCookiesServiceProvider extends ServiceProviderBase { }

Luckily for us all, core provides \Drupal\Core\DependencyInjection\ServiceProviderBase for us. This base class implements ServiceProviderInterface and ServiceModifierInterface to make it easier for us to modify the container. Let’s override the alter method so we can prepare to modify the commerce_cart.cart_session service.

<?php

namespace Drupal\commerce_cart_cookies;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

class CommerceCartCookiesServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    if ($container->hasDefinition('commerce_cart.cart_session')) {
        $container->getDefinition('commerce_cart.cart_session')
        ->setClass(CookieCartSession::class)
        ->setArguments([new Reference('request_stack')]);
    }
  }

}

We update the definition for commerce_cart.cart_session to use our class name, and also change it’s arguments to reflect our dependency on the request stack. The default service injects the session handler, whereas we need the request stack so we can retrieve cookies off of the current request.

The cart session service will now use our provided when the container is rebuilt!

The project code can be found at https://github.com/mglaman/commerce_cart_cookies

Discover and read more posts from Matt Glaman
get started