PHP
Article

Exploring the Cache API in Drupal 8

By Daniel Sipos

Drupal 8 comes with many improvements over its predecessor we have grown to both love and hate. Next to prominent systems such as Views in core, configuration management or a useful translation service, there are also less known changes but that are equally important to know and use. One such improvement has been the cache API that solves many performance problems we have in Drupal 7.

drupal8wide

In this article, I want to shine a bit of light over the new cache API. To this end, we are going to look at how we can use it in our custom modules as we are encouraged to do so much more in Drupal 8.

Additionally, I have prepared a little demonstration in the shape of a module you can install for testing the impact of the cache API. It’s a simple page that in its rendering logic makes an external API call (to a dummy JSON endpoint) and caches its results. The page then displays the actual time it takes for this to happen, contrasting the external call time vs. the cached version time.

The new cache API

Cache concept.

Bins

The new cache API (with the default DatabaseBackend storage) is stored in multiple bins which map to tables that start with the prefix cache_. When interacting with the cache, we always start by requesting a cache bin:

$cache = \Drupal::cache();

Where $cache will be an instance of the DatabaseBackend object that represents the default bin (cache_default). To request a particular bin we pass in the name in the constructor:

$render_cache = \Drupal::cache('render');

Where $render_cache will represent the render cache bin (which is new in Drupal 8 and is supposed to improve render performance across the board).

As you can see, we are requesting the cache service statically using the \Drupal class. If we are working inside classes, it is best practice to inject the service from the container. You can do so by specifying as an argument to your service the relevant cache bin service (such as cache.default). Here you can get a list of all core services including the ones related to cache.

But for the sake of brevity, we will use it statically here.

Retrieving cached items

Once we know which bin we want to work with (for custom modules this will usually be the default bin), we can retrieve and store cache items.

$cache = \Drupal::cache()->get('my_value');

It’s that simple. $cache will be a stdClass object containing some metadata about the cache item plus the actual data available under the $cache->data property. The my_value parameter is the cache ID.

An important thing to keep in mind is that using the get() method without a second parameter will not return the cache item if it has been invalidated (either programatically or through expiration). Passing the boolean true as a second parameter will force it to return the data.

Storing cache items

Although storing new items in the cache is just as easy as retrieving them, we have more options when doing so. To store an item we use the set() method (instead of get() like before), a method that takes 2 mandatory parameters and 2 optional ones:

  • the cache ID (the string by which we can later reference the item)
  • the data (a PHP value such as a string, array or object that gets serialised automatically and stored in the table – should not be over 1MB in size)
  • the expiration time (a timestamp in the future when this cache item will automatically become invalid or -1 which basically means this item never expires. It is best practice to use the Drupal\Core\Cache\CacheBackendInterface::CACHE_PERMANENT constant to represent this value)
  • tags (an array of cache tags this item can be later identified by)

As an example:

Drupal::cache()->set('my_value', $my_object, CacheBackendInterface::CACHE_PERMANENT, array('my_first_tag', 'my_second_tag'));

This will set a permanent cache item tagged with 2 tags and store a serialised version of $my_object as the data.

Cache invalidation and removal

Cache invalidation means that the respective items are no longer fresh and are unreliable in terms of what data they hold. They will be removed at the next garbage collection which can also be called using the garbageCollection() method on the CacheBackend object.

As mentioned above, when storing a cache item we can specify an expiration time. When this time lapses, the cache item becomes invalid but still exists in the bin and can be retrieved. However, we can also invalidate items manually using the invalidate(), invalidateMultiple() or invalidateAll() methods on the CacheBackend object.

Removing items altogether can be done using the delete(), deleteMultiple() or deleteAll() methods. These actions also happen only on the bin the CacheBackend is wrapping and completely remove the respective table records.

Cache tags

Another cool new feature of the Cache API in Drupal 8 are the cache tags (the fourth parameter in the setter method). The role of the tags is to identify cache items across multiple bins for proper invalidation. The purpose is the ability to accurately target multiple cache items that contain data about the same object, page, etc. For example, nodes can appear both on a page and in a view (stored in different cache items in different bins but both tagged with the same node:nid formatted tag). This allows invalidating both cache items when changes happen to that node without having to know the cache ids.

To manually invalidate caches using the tags, we can use the invalidateTags() method statically on the \Drupal\Core\Cache\Cache class:

\Drupal\Core\Cache\Cache::invalidateTags(array('node:5', 'my_tag'));

This will call the cache invalidator service and invalidate all the cache items tagged with node:5 and my_tag.

Additionally, for Drupal entities we don’t have to create our own tags but can retrieve them from the entity system:

  • \Drupal\Core\Entity\EntityInterface::getCacheTags()
  • \Drupal\Core\Entity\EntityTypeInterface::getListCacheTags()

This keeps the tags for Drupal entities consistent across the board.

Demonstrating the cache API

As I mentioned before, I created a small module that allows us to see the benefits of caching data. You can find the module in this git repository but here is the crux of it:

Please note that in this example I access the cache backend service statically to save some space. For a dependency injection approach (the correct approach), take a look at the repository code.

A route file that adds a new route to the /cache-demo path:

cache_demo_page:
  path: 'cache-demo'
  defaults:
    _controller: '\Drupal\cache_demo\Controller\CacheDemoController::index'
    _title: 'Cache demo'
  requirements:
    _permission: 'access content'

And the controller class that returns the page inside src/Controller/CacheDemoController.php:

<?php

/**
 * @file
 * Contains \Drupal\cache_demo\Controller\CacheDemoController.
 */

namespace Drupal\cache_demo\Controller;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url;
use \GuzzleHttp\Client;

/**
 * Cache demo main page.
 */
class CacheDemoController extends ControllerBase {

  public function index(Request $request) {
    $output = array();

    $clear = $request->query->get('clear');
    if ($clear) {
      $this->clearPosts();
    }

    if (!$clear) {
      $start_time = microtime(TRUE);
      $data = $this->loadPosts();
      $end_time = microtime(TRUE);

      $duration = $end_time - $start_time;
      $reload = $data['means'] == 'API' ? 'Reload the page to retrieve the posts from cache and see the difference.' : '';
      $output['duration'] = array(
        '#type' => 'markup',
        '#prefix' => '<div>',
        '#suffix' => '</div>',
        '#markup' => t('The duration for loading the posts has been @duration ms using the @means. @reload',
          array(
            '@duration' => number_format($duration * 1000, 2),
            '@means' => $data['means'],
            '@reload' => $reload
          )),
      );
    }

    if ($cache = \Drupal::cache()->get('cache_demo_posts') && $data['means'] == 'cache') {
      $url = new Url('cache_demo_page', array(), array('query' => array('clear' => true)));
      $output['clear'] = array(
        '#type' => 'markup',
        '#markup' => $this->l('Clear the cache and try again', $url),
      );
    }

    if (!$cache = \Drupal::cache()->get('cache_demo_posts')) {
      $url = new Url('cache_demo_page');
      $output['populate'] = array(
        '#type' => 'markup',
        '#markup' => $this->l('Try loading again to query the API and re-populate the cache', $url),
      );
    }

    return $output;
  }

  /**
   * Loads a bunch of dummy posts from cache or API
   * @return array
   */
  private function loadPosts() {
    if ($cache = \Drupal::cache()->get('cache_demo_posts')) {
      return array(
        'data' => $cache->data,
        'means' => 'cache',
      );
    }
    else {
      $guzzle = new Client();
      $response = $guzzle->get('http://jsonplaceholder.typicode.com/posts');
      $posts = $response->json();
      \Drupal::cache()->set('cache_demo_posts', $posts, CacheBackendInterface::CACHE_PERMANENT);
      return array(
        'data' => $posts,
        'means' => 'API',
      );
    }
  }

  /**
   * Clears the posts from the cache.
   */
  function clearPosts() {
    if ($cache = \Drupal::cache()->get('cache_demo_posts')) {
      \Drupal::cache()->delete('cache_demo_posts');
      drupal_set_message('Posts have been removed from cache.', 'status');
    }
    else {
      drupal_set_message('No posts in cache.', 'error');
    }
  }

}

Inside the index() method we do a quick check to see whether the clear query parameter is present in the url and call the clearPosts() method responsible for deleting the cache item. If there isn’t one, we calculate how long it takes for the loadPosts() method to return its value (which can be either the posts from the cache or from the API). We use Guzzle to make the API call and when we do, we also store the results directly. Then we just output the duration of the call in milliseconds and print 2 different links depending on whether there is cache stored or not (to allow us to clear the cache item and run the API call again).

When you navigate to cache-demo for the first time, the API call gets made and the 100 posts get stored in the cache. You can then reload the page to see how long it takes for those posts to be retrieved from the cache. Upon doing that, you’ll have a link to clear the cache (by a page refresh with the clear query string) followed by another link which refreshes the page without the clear query string and that in turn makes the API call again. And on like that to test the contrast in duration.

Conclusion

In this article we’ve looked at how easy it is to use the Cache API in Drupal 8. There are some very simple class methods that we can use to manage cache items and it has become too straightforward for us not to start using it in our custom modules. I encourage you to check it out, play around with the API and see for yourself how easy it is to use.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.