Drupal 8 API: cache API

Pre-requisites

How caching stores data in backends and bins

The terminology relating to caching in Drupal can be confusing, so let's define a few terms first.

As discussed in the subject of types of information in Drupal, caching is not so much an information type in itself, as a strategy (or a layer) that improves efficiency of access to existing information types. Any information which needs interpretation, in between its raw state and how it's used, can have its non-raw states cached: computed arrays of data; rendered HTML markup; or responses from calls to remote APIs. Cache data can be given a date on which it expires; it can be declared "invalid", which means it's still retrievable in an emergency, but might be out of date; or it can be deleted entirely.

Information from different sources—for example, from different stages during the processing of a HTTP request—can be stored in different bins. These are a way of dividing up the caching layer, so that a given bin could be emptied (or lost somehow) leaving other bins intact. Rendered HTML could all be stored in one bin, which could be emptied, whereas the details of what Drupal modules provide which functionality could be stored in another bin.

Cache bins can then each be stored in different, configurable backends. A backend is a storage mechanism: a SQL database, or Memcache, or even files on disk. Some bins might suit storage in the database; others might need quicker access and hence be better suited to storing in memory. Cache backends can even be chained, so that a transient memory cache sits on top of a database cache: the first request from the database cache is slow, but subsequent requests retrieve data from memory.

Finally, cached data can be tagged, with one or more short strings of text explaining what programmatic or real-world objects the data pertains to. For example, the assembled data for a node with ID=5, plus its rendered HTML, could be cached separately, in separate bins, and tagged with "node:5". When the node is edited, all cache items, across all bins, which pertain to the node will be invalidated together, by "invalidating the tag."

Cache and backend services

To manipulate cache items in Drupal, you retrieve the service pertaining to a particular bin, usually by dependency injection (DI), and make requests to that service: the service you need for a particular bin should usually be called cache.$bin_name. Cache items can be invalidated based on tags, using the cache_tags.invalidator service.

Confusingly, Drupal services named cache.backend.* are actually backend factories, for creating backends which are then themselves retrieved using service names cache.*. Here's an example, from core.services.yml:

  # (...)
  cache.backend.chainedfast:
    class: Drupal\Core\Cache\ChainedFastBackendFactory
    arguments: ['@settings']
    calls:
      - [setContainer, ['@service_container']]
  # (...)
  cache.config:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
      - { name: cache.bin, default_backend: cache.backend.chainedfast }
    factory: cache_factory:get
    arguments: [config]

In this example, cache.config is a service which returns CacheBackendChainedFast objects; these objects are created using the factory object service cache.backend.chainedfast
called with the arguments [config]; the factory's own constructor takes the site settings (defined in part in settings.php) and DI container, in order to build backend objects. This sounds complex, and the naming convention is slightly counterintuitive, but the end result is that a service called cache.$bin_name will be able to set, get, invalidate or delete cached items within the bin of that name.

In practice, this means that the service providing what we might lazily refer to as "the cache" is effectively a backend, configured to focus on retrieving data from only one bin.

Note that, at the time of writing, a service of the name cache.$bin_name must also be configured to provide arguments: [$bin_name]: these strings must be the same. Specifying different strings in the two locations can lead to undefined behaviour e.g. the choice of backend is not respected. See the bug report later on, in "Further reading etc."

Example custom cache bin

For the purposes of later worked examples, let's define a custom cache bin d8api. As discussed above, this can be done straightforwardly by appending the following to d8api.services.yml in the d8api module:

services:
  # (...)
  cache.d8api:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
      - { name: cache.bin, default_backend: cache.backend.memory }
    factory: cache_factory:get
    arguments: [d8api]

This cache bin uses almost entirely the default configuration for cache bins, while also declaring it should use the memory backend. This means that cached items are stored in memory for each PHP process or HTTP request, and discarded when the process is terminated.

Example class to perform cache manipulations

In the d8api module, save the following class as src/ExampleCacheUsage.php:

<?php
 
namespace Drupal\d8api;
 
use \Drupal\Core\Cache\CacheBackendInterface;
use \Drupal\Core\Cache\CacheTagsInvalidatorInterface;
 
/**
 * Run command-line tests of cache behaviour.
 */
class ExampleCacheUsage {
 
  /**
   * @var CacheBackendInterface
   *   Cache backend.
   */
  protected $cacheBackend;
 
  /**
   * @var CacheTagsInvalidatorInterface
   *   Cache backend.
   */
  protected $cacheTagsInvalidator;
 
  /**
   * Implements __construct().
   *
   * @param CacheBackendInterface
   */
  public function __construct(
    CacheBackendInterface $cacheBackend,
    CacheTagsInvalidatorInterface $cacheTagsInvalidator = null
  ) {
    $this->cacheBackend = $cacheBackend;
    $this->cacheTagsInvalidator = $cacheTagsInvalidator;
  }
 
  /**
   * Cache data to custom cache bin; invalidate it; delete it.
   */
  public function cacheInvalidateAndDelete() {
    // Demonstrate cache is empty, then filled.
    print "Found value:\t" . $this->returnValue('temp') . "\n";
    $this->cacheAndSay('temp', 'temporary value');
    print "Found value:\t" . $this->returnValue('temp') . "\n";
 
    // Invalidate; can still retrieve if we have to.
    $this->cacheBackend->invalidate('temp');
    print "Invalidated:\t" . $this->returnValue('temp') . "\n";
    print "Get invalid?:\t" . $this->cacheBackend->get('temp', true)->data . "\n";
 
    // Delete; has gone away.
    $this->cacheBackend->delete('temp');
    print "Deleted:\t" . $this->returnValue('temp') . "\n";
    print "Get invalid?:\t" . $this->cacheBackend->get('temp', true)->data . "\n";
  }
 
  /**
   * Update cache continuously, to show how different backends work.
   */
  public function updateCacheContinuously() {
    print "Previously:\tupdate=" . $this->returnValue('update') . "\n";
    $this->cacheAndSay('update', date('c'));
    print "Now set to:\tupdate=" . $this->returnValue('update') . "\n";
  }
 
  /**
   * Use different bins and try to access cache_config:system.site.
   */
  public function whichBin() {
    if ($config = $this->returnValue('system.site')) {
      print "Found config: sitename is '" . $config['name'] . "'\n";
    }
    else {
      print "Couldn't find config; right bin?\n";
    }
  }
 
  /**
   * Use cache tags.
   */
  public function invalidateViaTag() {
    $this->cacheBackend->set(
      'via_tag',
      'data cached using a tag',
      CacheBackendInterface::CACHE_PERMANENT,
      ['d8api:test']
    );
 
    print "Tagged item:\tvia_tag=" . $this->returnValue('via_tag') . "\n";
    $this->cacheTagsInvalidator->invalidateTags(['d8api:test']);
    print "Invalidated:\tvia_tag=" . $this->returnValue('via_tag') . "\n";
  }
 
  /**
   * Cache a string value and say we've done it.
   *
   * @param string $cid
   *   Cache ID.
   * @param string $string
   *   Value to cache.
   */
  private function cacheAndSay($cid, $string) {
    $this->cacheBackend->set($cid, $string);
    print "Cached $cid:\t$string\n";
  }
 
  /**
   * Return a value from the cache, or null.
   *
   * @param string $cid
   *   Cache ID.
   * @return mixed
   *   Data property of cache object.
   */
  private function returnValue($cid) {
    if ($cacheObject = $this->cacheBackend->get($cid)) {
      return $cacheObject->data;
    }
  }
 
}

This class contains a number of public functions that we're going to call at the command line later:

cacheInvalidateAndDelete()
Cache an item, then retrieve it; invalidate the item, and show that it can only be retrieved "in desperation" by accepting invalid items; then delete the item, and show that it's no longer retrievable.
updateCacheContinuously()
Try to retrieve a cache item; set it; then try to retrieve it again. This demonstrates cache persistence: effectively permanent; only for the duration of a request; or not cached at all.
retrieveOtherConfigIfExists()
Try to retrieve a cache item that exists in some other Drupal cache bin: if present, interrogate it; if absent, suggest the wrong bin is being injected.
invalidateViaTag()
Cache an item, retrieve it, then invalidate it via a tag.

There are also two "helper" private functions, which are really just used to make the examples more readable.

What you should see

As explained previously, you should clear all caches, and have Drush installed to run the following examples at the command line.

Invalidation and deletion

Run the following:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache()))->cacheInvalidateAndDelete();'

This injects a service for the default cache bin (accessed in this case by \Drupal::cache() rather than dependency injection, for simplicity's sake) into our new custom object, then calls the method to test invalidation and deletion.

The output should look something like:

Found value:	
Cached temp:	temporary value
Found value:	temporary value
Invalidated:	
Get invalid?:	temporary value
Deleted:	
Get invalid?:	

After the item is invalidated, it can't be retrieved, except by trying to get invalid date; after deletion, the item can't be retrieved at all.

Cache persistence with different backends

Default database backend

Using the same default database backend as above, examine how cache items persist in it:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache()))->updateCacheContinuously();'
drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache()))->updateCacheContinuously();'
drush cr
drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache()))->updateCacheContinuously();'

This produces the following three responses:

Previously:	update=
Cached update:	2017-01-20T14:02:06+00:00
Now set to:	update=2017-01-20T14:02:06+00:00
#
Previously:	update=2017-01-20T14:02:06+00:00
Cached update:	2017-01-20T14:02:29+00:00
Now set to:	update=2017-01-20T14:02:29+00:00
#
Previously:	update=
Cached update:	2017-01-20T14:03:09+00:00
Now set to:	update=2017-01-20T14:03:09+00:00

The cached item persists across Drush PHP processes, until the cache is rebuilt with drush cr: at which point the item must be re-cached to be retrievable.

Memory backend

The d8api cache bin we defined above uses the memory backend; run the following:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("d8api")))->updateCacheContinuously();'
drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("d8api")))->updateCacheContinuously();'

However many times you run it, you should get a response with the same overall structure:

Previously:	update=
Cached update:	2017-01-20T14:11:26+00:00
Now set to:	update=2017-01-20T14:11:26+00:00
#
Previously:	update=
Cached update:	2017-01-20T14:11:33+00:00
Now set to:	update=2017-01-20T14:11:33+00:00

The cache item is discarded at the end of each PHP process, and must be re-set in the next process.

Null, non-persisting backend

For development purposes only, Drupal can provide a cache.backend.null which acts like a null device: a cache with no lifetime, where items are immediately discarded! This is available in a separate services.yml that you must explicitly include. Interestingly, this is a recognized method for temporarily disabling any particular cache bin during development; for example, debugging of Twig templates can be made difficult by the Render API's cache, which is therefore best backended by null.

To experiment with the null backend, add the following two lines to the end of your site's settings.php:

<?php
/* (...) */
$settings['container_yamls'][] = DRUPAL_ROOT . '/sites/development.services.yml';
$settings['cache']['bins']['d8api'] = 'cache.backend.null';

This includes the development services, and modifies the cache bin settings, so that our custom bin uses the null backend.

Ironically, you'll need to clear the cache with drush cr for Drupal to see the new services; but do so, and then run the following:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("d8api")))->updateCacheContinuously();'

However many times you run it, the cache item will never return:

Previously:	update=
Cached update:	2017-01-20T14:12:10+00:00
Now set to:	update=

Make sure you comment out or remove the lines from your settings.php before you continue.

Retrieving data from some other cache bin

If you've cleared any caches recently, you should e.g. visit the site, to warm up Drupal's other cache bins, like cache.config. Run the following four commands (note the drush cr before the last php-eval):

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache()))->retrieveOtherConfigIfExists();'
drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("d8api")))->retrieveOtherConfigIfExists();'
drush cr
drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("config")))->retrieveOtherConfigIfExists();'

All three of the php-eval commands return the same message:

Couldn't find config; right bin?
Couldn't find config; right bin?
Couldn't find config; right bin?

Even if we inject the config cache, our custom code can't find the relevant information: how come? Well, immediately prior to accessing it, we ran another drush cr, which prompts Drupal to rebuild the caches (and leaves the config bin temporarily empty.) Immediately, visit the website in the browser, and then run the last command again:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("config")))->retrieveOtherConfigIfExists();'

You should find that the cache bin now includes the relevant item, and the site name can be retrieved:

Found config: sitename is 'Tutorial'

Your own site name may of course differ.

Tag-based invalidation

Our last example is the first one where two services must be injected into our custom object's constructor:

drush php-eval '(new Drupal\d8api\ExampleCacheUsage(\Drupal::cache("d8api"), \Drupal::service("cache_tags.invalidator")))->invalidateViaTag();'

The cache_tags.invalidator service will straightforwardly invalidate a tag, across all bins:

Tagged item:	via_tag=data cached using a tag
Invalidated:	via_tag=

If you can see all of these results, then congratulations! you have learnt how to take advantage of caching and cache invalidation methods in Drupal 8.

Further reading etc.

Add new comment