Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The cache

To make pages open fast, Drupal stores finished output and returns it on the next request without running your code. That is the cache. A module must say what its output depends on — otherwise visitors see stale data or someone else's data. The description is written right in the render array, in the #cache property.

Three questions

PropertyThe question it answersExample
tagsWhich data the output depends onnode:5, node_list:article
contextsFor whom and where the output differsuser, user.roles, url.query_args
max-ageFor how many seconds the output is valid3600, 0

Tags

A tag links output to data. When the data changes, Drupal invalidates everything marked with its tag. The /blog page depends on the list of articles:

// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
    // … the query and loading of articles from the lesson about queries
 
    $build = $this->entityTypeManager()
        ->getViewBuilder('node')
        ->viewMultiple($nodes, 'teaser');
 
    $build['#cache']['tags'][] = 'node_list:article';
 
    return $build;
}

The node_list:article tag is invalidated when any article is created, changed or deleted. Without it a new article would not appear on the /blog page until the cache is rebuilt. Each entity has its own tags too: $node->getCacheTags() returns ['node:5'].

Contexts

A context says the output comes in variants. The page shows the user name — so each user needs a separate variant:

// web/modules/custom/blog/src/Controller/BlogController.php
$build['hello'] = [
    '#markup' => $this->t('Hello, @name!', ['@name' => $this->currentUser()->getDisplayName()]),
    '#cache' => ['contexts' => ['user']],
];
ContextThe variant depends on
userThe particular user
user.rolesThe set of roles
user.permissionsThe set of permissions
url.pathThe page address
url.query_argsThe parameters after the question mark
languagesThe language

Choose the most general one that fits: user.permissions gives one variant for everyone with the same permissions, while user gives a separate one for each person.

Max-age

// web/modules/custom/blog/src/Controller/BlogController.php
$build['rates'] = [
    '#markup' => $this->t('Subscribers: @count', ['@count' => $count]),
    '#cache' => ['max-age' => 3600],
];

After an hour the output is built again. 0 means "do not cache". By default the lifetime is unlimited — the output lives until a tag is invalidated.

Your own data in the cache

You can cache not only output but also the result of slow work — for example the response of an external service:

// web/modules/custom/blog/src/Controller/BlogController.php
$cache = \Drupal::cache();
 
if ($cached = $cache->get('blog:stats')) {
    $stats = $cached->data;
} else {
    $stats = $this->loadStats();
 
    $cache->set('blog:stats', $stats, time() + 3600, ['blog_stats']);
}

get() returns an object with the data in its data property, or FALSE if there is no entry. The arguments of set() are the entry name, the data, the expiry time and the tags. loadStats() is your own method with the slow calculation.

Invalidating by tag

// anywhere, for example in submitForm of the subscription form
use Drupal\Core\Cache\Cache;
 
Cache::invalidateTags(['blog_stats']);

The line invalidates everything marked with blog_stats: the entry above and any output with this tag. That is how the cache is cleared precisely when your own data has changed.

Common mistake

A block outputs the name of the current user, and every visitor sees the name of whoever opened the page first. The cause is a missing context: Drupal stored one variant for everyone. If the output depends on the user, role, language or an address parameter, add a context; if it depends on data, add its tags. And do not cure such errors with 'max-age' => 0: it switches off the cache of the whole page.

Thanks for reading!

Donate to the site — we work hard for you.