Drupal 9: Using The Caching API To Store Data

The cache system in Drupal has a number of different components with time, contexts and tags being used to determine the cache.

Whilst all of these mechanisms are really useful you can also inject data directly into the cache system. This is beneficial if you have an expensive function that takes a long time to complete. The result of the function can be stored in the Drupal cache system just like other cache data.

For example, let's say you are pulling data from a slow API. If the data doesn't change much it might be better to add a cache to the request so that instead of going to the slow API the fast cache systems are used instead.

To get an instance of the cache system you can use the \Drupal::cache() method, which will return an instance of a CacheBackendInterface type object. By default, this will assume that you want to use the 'default' cache bin, which is essentially the cache_default table (assuming you are using the database for your cache storage).

Once you have a cache object, there are a couple of important methods you can use.

Cache Methods

The first method to talk about is the set() method, which is used to write some cache to the cache system. This takes a few parameters and is called like this.

\Drupal::cache()->set($cacheId, $data, Cache::PERMANENT, $cacheTags);

Let's go through the parameters a little.

  • $cacheId - This is s string that is used to identify the cache data in your cache system. This will be used later to read the cache data back out.
  • $data - The second parameter is just the data you want to cache. This can be a simple vale or a more complex array. The data is automatically serialised so as long as the data is serialiseable then the data it fine to save.
  • Cache::PERMANENT - The third parameter is the length of time to keep the cache for. You can either add what I have added here (the Cache::PERMANENT constant, which will store the cache forever) or you can add a timestamp. The timestamp tells Drupal the time that the cache should be considered expired. Expired cache items are periodically cleared by Drupal's garbage collection systems. This is an optional argument and Cache::PERMANENT is assumed if this is left out.
  • $cacheTags - You can optionally pass in an array of cache tags. These are used by Drupal to invalidate this cache item when other things on the site change. For example, you can pass in the tag "node:1" so that if the entity node 1 is changed then this cache item is invalidated so that new caches can be created.

Conversely, the get() method is used to get some data from the cache system. This method accepts the cache ID you want to get.

$output = \Drupal::cache()->get($cacheId);

This method will either return "false" if the cache is not found, or a stdClass object that contains the cache data saved in the "data" parameter.

The get() method is normally used within an if statement so that if the result of the get() method is then passed upstream, rather than continuing the execution of the function.

if ($cache = \Drupal::cache()->get($cacheId)) {
  return $cache->data;
}

You can also pass a second parameter to force the get() method to allow invalidated cache items to be returned. This is still used in the same way.

if ($cache = \Drupal::cache()->get($cacheId, TRUE)) {
  return $cache->data;
}

This is useful in certain situations where the cache takes a long time to regenerate and you need to be sure that something is retrieved from the cache, even if it is out of date. You can then re-generate the cache through other processes.

Putting Things Together

These methods are normally put together in the following way. If the get() method returns a false value then we continue to generate the cache data and save this into the cache using the set() method.

$cacheId = 'my_cache';

if ($cache = \Drupal::cache()->get($cacheId)) {
  return $cache->data;
}

$data = ['some data to cache'];

\Drupal::cache()->set($cacheId, $data, Cache::PERMANENT);

return $data;

You can add this template of code wherever you need to actively store things in Drupal's cache system.

An Example Of Adding The Cache API To A Slow Block

As an example of how this system can be used to solve some slow code I have created a simple Drupal block.

The following block disables the page cache so that it can regenerate some output to the page on every page load. This works out the number of nodes on the site using a very inefficient method and then returns that value to a render array to print out. This might seem like an arbitrary way to slow down a site, but I saw a site that contained similar code a few years ago that caused the site to be very slow indeed. The code in question can be anything that slows down the rendering process, including API lookups, slow database queries or simply processes that take a long time to execute.

<?php

namespace Drupal\mymodule\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Cache\Cache;

/**
 * Provides a block to cache heavy content.
 *
 * @Block(
 *   id = "cachedblock",
 *   admin_label = @Translation("Cached Block"),
 * )
 */
class CachedBlock extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a new BookNavigationBlock instance.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $build = [];

    // Disable the page cache.
    \Drupal::service('page_cache_kill_switch')->trigger();

    $build['output'] = [
      '#markup' => '<p>' . $this->expensiveMethod() . '</p>',
    ];

    return $build;
  }

  public function expensiveMethod() {
    // Work out the number of nodes available on the site.
    $count = 0;

    $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple();

    foreach ($nodes as $node) {
      $count++;
    }

    $output = $this->t('There are :count nodes on this site.', [':count' => $count]);

    return $output;
  }

  public function getCacheMaxAge() {
    // Set the cache timeout to be 0.
    return 0;
  }

}

By adding this block to the homepage we create a situation where the more pages there are on the site, the slower the page will load. Performance analysis will easily show that this expensiveMethod() takes a while to run.

We can start improving the code by adding the cache template so that we save the output of the calculation to the cache. With this in place, if there is anything in the cache it will be returned instead of re-doing the calculation.

  public function expensiveMethod() {
    $cacheId = 'mymodule:cachedblock';

    if ($cache = \Drupal::cache()->get($cacheId)) {
      return $cache->data;
    }

    // Work out the number of nodes available on the site.
    $count = 0;

    $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple();

    foreach ($nodes as $node) {
      $count++;
    }

    $data = $this->t('There are :count nodes on this site.', [':count' => $count]);

    \Drupal::cache()->set($cacheId, $data, time() + 86400);

    return $data;
  }

The cache here is set to be 86400 seconds (i.e. 1 day) into the future, which means that the cache we set will regenerate every day and show the latest count of the number of nodes.

We can further improve this by adding cache tags to the set() method. Since we are looking at nodes we can use the individual "node:n" cache tag for each of the nodes being looked at and the "node_list" cache tag for the list of nodes as a whole. The Drupal documentation contains information about what cache tags are available.

With the cache tags array created we then pass it to the set() method.

  public function expensiveMethod() {
    $cacheId = 'mymodule:cachedblock';

    if ($cache = \Drupal::cache()->get($cacheId)) {
      return $cache->data;
    }

    $cacheTags = [];

    // Work out the number of nodes available on the site.
    $count = 0;

    $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple();

    foreach ($nodes as $node) {
      $count++;
      $cacheTags[] = 'node:' . $node->id();
    }

    $cacheTags[] = 'node_list';

    $data = $this->t('There are :count nodes on this site.', [':count' => $count]);

    \Drupal::cache()->set($cacheId, $data, time() + 86400, $cacheTags);

    return $data;
  }

By setting the cache tags we are informing Drupal what information this cache contains. This means that if we add or remove one of the nodes the cache will be invalidated.

This method is a good way of solving poorly performing code and will produce immediate performance gains for situations where the cache is bypassed and slow code is executed. With this in simple change in place at least one user a day will see a slow page response, but the general state of the page will be much more performant. This, at least, allows you to work at improving the underlying performance problem but still allow the site to function.

More in this series

Comments

Nice example and a good reminder that I should probably be using cache more oftern than I am.

Permalink

I wish it were a bit more intuitive to use Drupal's caches. I had so much trouble getting the hang of it all when I first started trying to cache custom data, so I ended up writing a module to help make it a bit simpler: https://www.drupal.org/project/cache_register

Also, there's a "better" way to do $cacheTags[] = 'node:' . $node->id();> Entities have a getCacheTags() method, so you can do like:
 

$tags = [];

foreach($nodes as $node) {
  $tags = \Drupal\Core\Cache::mergeTags($node->getCacheTags());
}

 

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
13 + 3 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.