CacheImg
2019 29 Jul

How does entity cache work in Drupal 8

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 <entity type ID>:<entity ID>

'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.

<?php

namespace Drupal\custom_plugin\Controller;

use Drupal\user\Entity\User;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Cache\Cache;

/**
 * Class CacheController.
 */
class CacheController extends ControllerBase {

  /**
   * The cache backend service.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cacheBackend;

  /**
   * Constructs a new CacheController object.
   */
  public function __construct(CacheBackendInterface $cache_backend) {
    $this->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

<div class="custom-plugin-block">
  <p>User ID: {{ user }}</p>
  <p>{{ data }}</p>
</div>

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. You can also check about Drupal's Latest version 10 features and what's beyond here

Latest Blogs

Blockchain Integration Into Public Digital Good

The Role of Blockchain in Digital Public Goods: Use Cases and Innovations

Digital public goods, such as open-source software, IT models, and standards, are the backbone of our digital infrastructure.

Read More

Role of Open Source and Digital Public Goods

Boost DPG Development: How Open Source Maximizes Efficiency

The emergence of open-source workflow solutions has revolutionized workflow management.

Read More

Digital Public Goods Alliance Strategy 2021–2026

Boosting Digital Infrastructure with Digital Public Goods

The United Nations (UN) defines a roadmap for Digital Public Goods (DPGs) as open-source software, open data, open AI models, open standards, and open content.

Read More

Best Practices for Software Testing

Power of Software Testing: Why Software Teams Use It Effectively

In the modern digital era, where software is used in nearly every aspect of everyday life, the importance of software testing cannot be emphasized.

Read More