Out of Stock feature in Drupal Commerce 2.x

Implementing the Out of Stock feature in Drupal Commerce 2.x is easy. Unfortunately, you can't just enable this feature, because Commerce doesn't support it out of the box. You have to get your hands dirty and write some code.

As you may know, there is a contrib stock module for Drupal 8 and 9, but it's still in the development phase, so I wouldn't recommend you to use it in production.

Let's see how in three simple steps you can add the Out of Stock feature to your webshop.

Step 1

Go to your Product variation fields configuration (/admin/commerce/config/product-variation-types/default/edit/fields) and add a new integer field. Call it Stock or something like that. The value of this field will dictate if the product is in stock or not.

Step 2

Implement the AvailabilityChecker service. Inside your MODULE_NAME.services.yml file add this:

services:
  MODULE_NAME.variation_availability_checker:
    class: Drupal\MODULE_NAME\VariationAvailabilityChecker
    tags:
      - { name: commerce.availability_checker, priority: 100 }

You can now create a class file inside the src directory. The name of this file should be VariationAvailabilityChecker.php

<?php

namespace Drupal\MODULE_NAME;

use Drupal\commerce\AvailabilityCheckerInterface;
use Drupal\commerce\Context;
use Drupal\commerce\PurchasableEntityInterface;

/**
 * Class VariationAvailabilityChecker.
 */
class VariationAvailabilityChecker implements AvailabilityCheckerInterface {

  /**
   * {@inheritdoc}
   */
  public function applies(PurchasableEntityInterface $entity) {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function check(PurchasableEntityInterface $entity, $quantity, Context $context) {
    if (intval($entity->get('field_stock')->value <= 0) {
      return FALSE;
    }
    return TRUE;
  }

}

As you can see, this is a super simple class. We have only two methods. applies() is used to determine whether the checker applies to the given purchasable entity. Maybe you have several variation types and you want to apply the checker to only one of them. In our case, we just return TRUE. That means that the checker will be always applied.

The second method is check(). This method checks the availability of the given purchasable entity. If we return TRUE that means that the entity is available, and FALSE means that it is unavailable. Logic in our example is quite simple. If the value of the Stock field is equal to or less than zero then return FALSE and make the variation unavailable.

Step 3

The last step is to alter the Add to cart form and use the AvailabilityChecker service to enable/disable the Add to cart button. Inside your MODULE_NAME.module file add this:

use Drupal\commerce\Context;
use Drupal\commerce_product\Entity\ProductVariation;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_alter().
 */
function MODULE_NAME_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (strpos($form_id, 'commerce_order_item_add_to_cart_form') !== FALSE) {
    $selected_variation_id = $form_state->get('selected_variation');
    $product_variation = ProductVariation::load($selected_variation_id);

    /** @var \Drupal\MODULE_NAME\VariationAvailabilityChecker $variation_availability_checker */
    $variation_availability_checker = \Drupal::service('MODULE_NAME.variation_availability_checker');

    $current_user = \Drupal::currentUser();
    $current_store = \Drupal::service('commerce_store.current_store')->getStore();
    $context = new Context($current_user, $current_store);

    if (!$variation_availability_checker->check($product_variation, 1, $context)) {
      $form['actions']['submit']['#value'] = t('Out of stock');
      $form['actions']['submit']['#disabled'] = TRUE;
    }
  }
}

In the form alter we are calling the check() method to see if the currently selected variation is available. We are passing three arguments: $product_variation, 1, $context. Since we didn't implement any logic regarding the quantity, we can pass any value, but here we are passing 1.

You don't have to implement step 3. It's optional but it greatly improves the user experience. If you don't disable the Add to cart button, users will be able to click on it and they will get the message that says that product has been added to the cart, but that won't happen. The product won't be added to the cart and in my opinion that is really confusing.

Also, make sure that your products do have some attributes, otherwise, the following line won't work and you will have to refactor my code a little bit:

$selected_variation_id = $form_state->get('selected_variation');

You can vastly improve upon this example and create even better stock management features. You can for example automatically decrease the Stock field value each time someone buys an item. To do this you can subscribe for example to the ORDER_PAID event and add some logic to decrease the stock value for all ordered products.

Here is what the module structure should look like:

my_module
├── my_module.info.yml
├── my_module.module
├── my_module.services.yml
└── src
    └── VariationAvailabilityChecker.php

Please note that Drupal 8 adheres to PSR-4. This means that files must be named in certain ways and placed in specific folders.

May 29th, 2021 - Availability manager - Update

Since version 2.19 AvailabilityManager API has moved to the Commerce Order module. This means that the commerce.availability_manager service is deprecated in favor of the commerce_order.availability_manager service. You can find more information in the change record and the original issue.

This change is welcomed because AvailabilityManager API is now order aware. The old availability manager was very limited because it didn't allow the availability checkers to return an error message and didn't provide access to the order item we're checking the availability for.

The updated code for Step 2 and 3:

MODULE_NAME.services.yml

services:
  MODULE_NAME.variation_availability_checker:
    class: Drupal\MODULE_NAME\VariationAvailabilityChecker
    tags:
      - { name: commerce_order.availability_checker, priority: 100 }

VariationAvailabilityChecker.php 

<?php

namespace Drupal\MODULE_NAME;

use Drupal\commerce\Context;
use Drupal\commerce_order\AvailabilityCheckerInterface;
use Drupal\commerce_order\AvailabilityResult;
use Drupal\commerce_order\Entity\OrderItemInterface;

/**
 * Class VariationAvailabilityChecker.
 */
class VariationAvailabilityChecker implements AvailabilityCheckerInterface {

  /**
   * {@inheritdoc}
   */
   public function applies(OrderItemInterface $order_item) {
     return TRUE;
   }

  /**
   * {@inheritdoc}
   */
  public function check(OrderItemInterface $order_item, Context $context) {
    $entity = $order_item->getPurchasedEntity();
    
    if (intval($entity->get('field_stock')->value) <= 0) {
      return AvailabilityResult::unavailable('This product is out of stock.');
    }

    return AvailabilityResult::neutral();
  }

}

MODULE_NAME.module

use Drupal\commerce\Context;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_alter().
 */
function MODULE_NAME_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (strpos($form_id, 'commerce_order_item_add_to_cart_form') !== FALSE) {
    $order_item = $form_state->getFormObject()->getEntity();

    /** @var \Drupal\MODULE_NAME\VariationAvailabilityChecker $variation_availability_checker */
    $variation_availability_checker = \Drupal::service('MODULE_NAME.variation_availability_checker');

    $current_user = \Drupal::currentUser();
    $current_store = \Drupal::service('commerce_store.current_store')->getStore();
    $context = new Context($current_user, $current_store);

    if ($variation_availability_checker->check($order_item, $context)->isUnavailable()) {
      $form['actions']['submit']['#value'] = t('Out of stock');
      $form['actions']['submit']['#disabled'] = TRUE;
    }
  }
}

About the Author

Goran Nikolovski is an experienced web and AI developer skilled in Drupal, React, and React Native. He founded this website and enjoys sharing his knowledge.