Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

How does entity cache work in Drupal 8

Parent Feed: 

The Drupal Cache API is used to store data that takes a long time to compute. Caching can either be permanent or valid only for a certain time span, and the cache can contain any type of data. To make websites faster Drupal stores web pages in a cache.

Drupal Cache has three properties

  • Cache context creates context variations when render arrays are being generated. If we have user as a context, every user may not have the same user permissions or language.
  • Cache tags define what object the cache depends on. For dependencies on data managed by Drupal, like entities and configuration.
  • Cache max-age is the maximum time that the cache is stored.

Here is an example from a custom block in Drupal 8:

use Drupal\Core\Cache\Cache;

return [
      '#theme' => 'user_profile_template',
      '#user_data' => $user_data,
      '#cache' => [
        'tags' => ['languages', 'timezone'],
        'contexts' => ['node:5', 'user:3'],
        'max-age' => Cache::PERMANENT,
      ],
];

Use of cache tags in Entity Caching

The cached data in different bins becomes old and obsolete at some point of time and requires removal from these bins to accommodate the latest changes. Before Drupal 8, there was no way to identify individual pieces of expired data stored in different cache bins. 

Cache tags provides a way to track which cache items depend on some data managed by Drupal.
If a renderable output which is output of a Controller or a custom block depends on content provided by some entity, we use cache tags to invalidate the data.
For example, if a node is updated, which appear in two views and three blocks. Without cache tags we wouldn't know which cache item to invalidate

The syntax for setting cache tags is thing:identifier. It has to be unique string and cannot contain spaces.

Entities gets caches in the form of :

'tags' => ['node_list'], //invalidate when any node updates
'tags' => ['node:1','term:2'], //invalidate when node id 1 or term id 2 is updated
 

We can also define our own cache tag:

  • Request a cache object through \Drupal::cache().
  • Define a Cache ID (cid) value for your data. A cid is a string, which must contain enough information to uniquely identify the data.
  • Call the get() method to attempt a cache read, to see if the cache already contains your data.
  • If your data is not already in the cache, compute it and add it to the cache using the set() method.
$nid = 9;
$cid = 'my_module:' . $nid;

// Check if the cache already contain data.
if ($item = \Drupal::cache()->get($cid)) {
  return $item->data;
}

// The data to be cached.
$node = Node::load($nid);
$data = [
  'title' => sprintf('## %s', $node->get('title')->getValue()),
  //...
];

// Set the cache for 'my_module:' . $nid cache tag until $node changes.
\Drupal::cache()->set($cid, $data, Cache::PERMANENT, $node->getCacheTags());

A cache item can have multiple cache tags (an array of cache tags), and each cache tag is a string. Drupal associated cache tags with entity and entity listings. It is important to invalidate listings-based caches when an entity no longer exists or when a new entity is created. This can be done using EntityTypeInterface::getListCacheTags(), it enables code listing entities of this type to ensure that newly created entities show up immediately or invalidate the ones that don’t exist.

Cache::invalidateTags is used to invalidate all cached data of a certain cache tag.

// Invalidate all cache items with certain tags.
\Drupal\Core\Cache\Cache::invalidateTags(array('node:1',  'user:7'));

Explaining with an Example

Create a file custom_plugin/custom_plugin.services.yml in your custom module.

services:
  custom_plugin.my_cache:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
      - { name: cache.bin }
    factory: cache_factory:get
    arguments: [my_cache]

This is to declare our cache whose identifier will be my_cache.

Create a routing for your controller custom_plugin/custom_plugin.routing.yml

custom_plugin.cache:
  path: '/my-cache'
  defaults:
    _controller: 'Drupal\custom_plugin\Controller\CacheController::content'
    _title: 'Cache'
  requirements:
    _permission: 'access content'

Then in our Controller custom_plugin/src/Controller/CacheController.php we will create a custom cache tag to store a dynamic cache item with conditions to invalidate when the value is updated.

cacheBackend = $cache_backend;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('custom_plugin.my_cache')
    );
  }

  /**
   * Build the user dynamic data.
   *
   * @return array
   *   Return the render array of the user dynamic data.
   */
  public function content() {
    $user = User::load(\Drupal::currentUser()->id());

    // Create a custom cache tag.
    $cid = 'custom_plugin:' . $user->id();
    // Check if there is any cache item associated with this cache tag.
    $data_cached = $this->cacheBackend->get($cid);

    if (!$data_cached) {
      // Build the user dynamic data.
      $data = $user->getAccountName() . ' last accessed at ' . date('H:i', $user->getLastAccessedTime());

      // Merge the entity cache of an user entity with our custom tag.
      $tags = Cache::mergeTags(['user:' . $user->id()], [$cid]);

      // // Store the data into the cache.
      $this->cacheBackend->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags);
    }
    else {
      $data = $data_cached->data;
      $tags = $data_cached->tags;
    }

    // Return a renderable output.
    $build = [
      '#theme' => 'user_data',
      '#user' => $user->id(),
      '#data' => $data,
      '#cache' => [
        'tags' => $tags,
        'context' => ['user'],
      ],
    ];

    return $build;
  }

}

Initialize the variables to be used in the template in custom_plugin/custom_plugin.module file.

/**
 * Implements hook_theme().
 */
function custom_plugin_theme() {
  return [
    'user_data' => [
      'variables' => [
        'user' => [],
        'data' => [],
      ],
    ],
  ];
}

Create a template custom_plugin/templates/user-data.html.twig to print the cache items

User ID: {{ user }}

{{ data }}

Most developers and development teams have one cache invalidation strategy i.e. clear all cache. And that is not a good idea for complex websites and applications which have a huge amount of content. This custom cache invalidation strategy will help you clear only the required cache and keep the rest intact. This can boost the performance a lot and goes without saying, you have a better control of your site's cache.

Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web