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
| Property | The question it answers | Example |
|---|---|---|
tags | Which data the output depends on | node:5, node_list:article |
contexts | For whom and where the output differs | user, user.roles, url.query_args |
max-age | For how many seconds the output is valid | 3600, 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']],
];| Context | The variant depends on |
|---|---|
user | The particular user |
user.roles | The set of roles |
user.permissions | The set of permissions |
url.path | The page address |
url.query_args | The parameters after the question mark |
languages | The 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.