You are on page 1of 10

Drupal 8 Add Cache Metadata to

Render Arrays
Some operations are time consuming, really heavy memory and/or CPU intensive. By
performing an operation one time, and then caching the output, the next requests could
be executed faster. Drupal provides an easy cache API in order to store, retrieve and
invalidate cache data. I did this tutorial because I couldn’t find a step by step tutorial in
order to add cache metadata to render arrays easily!

In this tutorial we'll:


● Get an overview of the render array caches and how to use them properly.
● We are going to get our hands dirty on code.

Prerequisites
● Familiarity with custom module development.
● How to create a custom controller to process incoming requests.
● Some knowledge of render arrays.

Overview of Render array


Drupal uses render arrays to generate HTML that is presented to the end user. While
render arrays are a complex topic, let’s cover the basics. A render array is an
associative array that represents a one or more HTML elements, properties and values.
If you’re interested in more about render arrays, see Render arrays from official Drupal
docs".

Cache metadata to render array


When we have a render array, instructing Drupal to cache the results is easy, we only
need to use the #cache property. But what kind of caching? Drupal 8 provides several
kinds out of the box:

● max-age stores cache data by defining its time in integer format and seconds
● tags is an array of one or more cache tags identifying the data this element
depends on.
● contexts specifies one or more cache context IDs. These are converted to a final
value depending on the request. For instance, 'user' is mapped to the current
user's ID.

Creating the module and controller


Now that we have the basics of how to use #cache in a render array, let’s put it to use.
First, we’ll create a new custom module using Drupal Console:
$ drupal generate:module --machine-name=d8_cache

A module alone isn’t enough. We also need a controller to respond to incoming


requests. We can use Drupal Console to generate the controller too:

$ drupal generate:controller --module=d8_cache --class=DefaultController


When creating the controller, you’ll enter into a loop where you can
enter three pieces of information necessary for the controller to
define a route: The title, method name, and the path. Let’s make one
route for each of the cache types:

Title Method Name Path

cacheMaxAge cacheMaxAge /d8_cache/max-age

cacheContextsByUrl cacheContextsByUrl /d8_cache/contexts

cacheTags cacheTags /d8_cache/tags

Now we should have an *.info.yml, *.routing.yml and our controller class.inally, let’s enable our
custom module:

$ drupal module:install d8_cache

Cache “max-age”
With the module and routes created, we can now start playing with Drupal caching.In
DefaultController.php, locate the cacheMaxAge() method and add the following:

public function cacheMaxAge() {


return [
'#markup' => t('Temporary by 10 seconds @time', ['@time' => time()]),
'#cache' => [
'max-age' => 10,
]
];
}
If we open a web browser and navigate to http://your_drupal_site.test/d8_cache/max-age, we
see a “Temporary by 10 seconds timestamp” where timestamp is the current time as a UNIX
timestamp.
“What good is that!?” you might ask. Well, if you refresh the page you’ll notice something
interesting. The first time the page will say something like “Temporary by 10 seconds
1520173774”. If we hit refresh immediately, we’ll see:

“Temporary by 10 seconds 1520173780” (the first second)

“Temporary by 10 seconds 1520173780” (in the next second)

“Temporary by 10 seconds 1520173780” (and so on)

The timestamp doesn’t change! If we wait for the whole 10 seconds we specified in max-age,
the cache invalidates/expires and and is replaced with a new timestamp: “Temporary by 10
seconds 1520173790”

Great, this worked like a charm!

What if we want to make it so the page never expires? Drupal provides a special constant for
this, \Drupal\core\cache\Cache::PERMANENT exactly for this case. We’d only need to change
the value of max-age:

public function cacheMaxAge() {


return [
'#markup' => t('WeKnow is the coolest @time', ['@time' => time()]),
'#cache' => [
'max-age' => \Drupal\Core\Cache\Cache::PERMANENT,
]
];
}

And the message for instance “weKnow is the coolest 1520173780” will never change! Well, not
“never”. We can force the page to update by clearing the Drupal cache. This can be done under
Admin > Config > Development > Performance, or using Drupal Console:

$ drupal cr all

So that was max-age, one of the simplest caching strategies. What if we need
something more...nuanced?
Cache “contexts”
Caching by contexts let us specify a condition by which something remains cached. A simple
example is the URL Query, or any part after the ? in a URL. We already defined the route
earlier, so we open DefaultController.php and edit the cacheContextByUrl() method:

public function cacheContextsByUrl() {


return [
'#markup' => t('WeKnow is the coolest @time', ['@time' => time()]),
'#cache' => [
'contexts' => ['url.query_args'],
]
];
}

The above piece of code will display a message such as “weKnow is the coolest 1520173780”,
and invalidate cache when a new query parameter from url is set or gets updated.

If we visit for instance http://your_drupal_site.test/d8_cache/contexts the first time, we’ll see


something like: “weKnow is the coolest 1520173780”. If we hit again the same message is
displayed. But, if we do add a query parameter like
http://your_drupal_site.test/d8_cache/contexts?query_a=value, then the cache is invalidated
and the page updates with a new timestamp: “weKnow is the coolest 1520173909”.

Sometimes, we only want to invalidate the cache based on a specific argument in the URL query. We
can do that too:

public function cacheContextsByUrlParam() {


return [
'#markup' => t('WeKnow is the coolest @time', ['@time' => time()]),
'#cache' => [
'contexts' => ['url.query_args:your_query_param'],
]
];
}

Now if we visit the following URL:

http://your_drupal_site.test/d8_cache/contexts-param?your_query_param=value
Only then does the message change:“weKnow is the coolest 1520173909” If we visit the same
URL with the same query parameter set (your_query_param), the cache is invalidated
and we get a new timestamp once again:
“weKnow is the coolest 1520173910”

And so on…

The url.query_args:your_query_param value we passed to contexts in our render array


instructs Drupal to only invalidate the cache if a certain URL query parameter is set.

If we visit:

http://your_drupal_site.test/d8_cache/contexts-param?this_is_another_query_param=value

The message is “weKnow is the coolest 1520173910” (first second)

“weKnow is the coolest 1520173910” (next second)

“weKnow is the coolest 1520173910” (after few minutes)

And so on!

Notice the message doesn’t change. This is because we set to invalidate cache on the query
param “your_query_param” and above is another query param. Since your_query_param is
not in our URL, Drupal will never invalidate the cache.

Caching by the URL query isn’t the only context available in Drupal. There are several others:

● theme (vary by negotiated theme)


● user.roles (vary by the combination of roles)
● user.roles:anonymous (vary by whether the current user has the 'anonymous' role or not,
i.e. "is anonymous user")
● languages (vary by all language types: interface, content …)
● languages:language_interface (vary by interface language —
LanguageInterface::TYPE_INTERFACE)
● languages:language_content (vary by content language —
LanguageInterface::TYPE_CONTENT)
● url (vary by the entire URL)
● url.query_args (vary by the entire given query string)
● url.query_args:foo (vary by the ?foo query argument)

Refer to drupal 8 contexts official documentation for more details about cache “contexts”
https://www.drupal.org/docs/8/api/cache-api/cache-contexts

Cache “tags”
The contexts cache type is really versatile, but sometimes we need more complete control over
what is and isn’t cached. For that, there’s tags. Open the controller and modify the cacheTags()
method to be the following:
public function cacheTags() {
$userName = \Drupal::currentUser()->getAccountName();
$cacheTags = User::load(\Drupal::currentUser()->id())-
>getCacheTags();
return [
'#markup' => t('WeKnow is the coolest! Do you agree
@userName ?', ['@userName' => $userName]),
'#cache' => [
// We need to use entity->getCacheTags() instead of
hardcoding "user:2"(where 2 is uid) or trying to memorize each
pattern.
'tags' => $cacheTags,
]
];
}

Ok, now let’s login with our username -- this post uses “Eduardo” -- and visit:

http://your_drupal_site.test/d8_cache/tags

Above code prints “weKnow is the coolest! Do you agree Eduardo?” If we hit the page again it
will say “weKnow is the coolest! Do you agree Eduardo?” and subsequent requests will say the
same.

If we edit our own username to “EduardoTelaya” and hit save our tag cached page changes:

“weKnow is the coolest! Do you agree EduardoTelaya?”

Why is that?

If you look closely at the method, you’ll notice we get a list of cache tags for the current user. If
we use a debugger to see the value of $cacheTags, it will say “user:userID” where userID is the
user’s unique ID number. When we updated our user account, Drupal invalidated any cached
content associated with that tag. Cache tags let us build a dependency into our cache on
another entity or entities in the site. We can even define our own tags to have full control!

Tips and tricks

In the above examples we only had one #cache in each render array. Drupal allows us to
specify the caching at different levels in the tree depending on need. Let’s suppose we have the
following, a tree of render array:
public function cacheTree() {

return [
'permanent' => [
'#markup' => 'PERMANENT: weKnow is the coolest ' . time() . '<br>',
'#cache' => [
'max-age' => Cache::PERMANENT,
],
],
'message' => [
'#markup' => 'Just a message! <br>',
'#cache' => [
]
],
'parent' => [
'child_a' => [
'#markup' => '--->Temporary by 20 seconds ' . time() . '<br>',
'#cache' => [
'max-age' => 20,
],
],
'child_b' => [
'#markup' => '--->Temporary by 10 seconds ' . time() . '<br>',
'#cache' => [
'max-age' => 10,
],
],
],
'contexts_url' => [
'#markup' => 'Contexts url - ' . time(),
'#cache' => [
'contexts' => ['url.query_args'],
]
]
];
}

If we visit the first time http://your_drupal_site.test/d8_cache/tree:

We get this:

PERMANENT: weKnow is the coolest 1520261602

Just a message!

--->Temporary by 20 seconds 1520261602

--->Temporary by 10 seconds 1520261602

Contexts url - 1520261602


(Please refer to timestamp above for example purposes)

In the next second, if we visit the same page again, we get the same message. But once it
reaches 10 seconds, the cache is invalidated thanks to the render array element “child_b”
(which was set to expire/invalidate to 10 seconds) and we are going to have a different
message:

PERMANENT: weKnow is the coolest 1520261612

Just a message!

--->Temporary by 20 seconds 1520261612

--->Temporary by 10 seconds 1520261612

Contexts url - 1520261612

Notice how not only “child_b” was updated but also the rest of render array elements. The same
will happen if you wait 20 seconds or visit /d8_cache/tree?query=value, which invalidates cache
according to url query contexts.

This is called “bubbling up cache”. This can affect the response cache you can see as a whole!
In order to avoid that you should use “keys” attribute in order to cache individual elements. By
adding “keys” you protect from cache invalidation from siblings array elements and children
array elements. Let’s add a new method and path to our code in order to add keys:

public function cacheTreeKeys() {

return [
'permanent' => [
'#markup' => 'PERMANENT: weKnow is the coolest ' . time() . '<br>',
'#cache' => [
'max-age' => Cache::PERMANENT,
'keys' => ['d8_cache_permament']
],
],
'message' => [
'#markup' => 'Just a message! <br>',
'#cache' => [
'keys' => ['d8_cache_time']
]
],
'parent' => [
'child_a' => [
'#markup' => '--->Temporary by 20 seconds ' . time() . '<br>',
'#cache' => [
'max-age' => 20,
'keys' => ['d8_cache_child_a']
],
],
'child_b' => [
'#markup' => '--->Temporary by 10 seconds ' . time() . '<br>',
'#cache' => [
'max-age' => 10,
'keys' => ['d8_cache_child_b']
],
],
],
'contexts_url' => [
'#markup' => 'Contexts url - ' . time(),
'#cache' => [
'contexts' => ['url.query_args'],
'keys' => ['d8_cache_contexts_url']
]
]
];
}

If we visit now /d8_cache/tree-keys

We will get:

PERMANENT: weKnow is the coolest 1520261612

Just a message!

--->Temporary by 20 seconds 1520261612

--->Temporary by 10 seconds 1520261612

Contexts url - 1520261612

And if we wait for 10 seconds we are going to see:

PERMANENT: weKnow is the coolest 1520261612

Just a message!

--->Temporary by 20 seconds 1520261612

--->Temporary by 10 seconds 1520261622

Contexts url - 1520261612


Notice how just “--->Temporary by 10 seconds 1520261622” gets updated but the rest of the
output doesn’t get updated (this is thanks to keys attribute that prevent cache invalidation to the
rest of array elements)

Download:
You can download full source code for this post on Github.

Recap
In this post, we saw an overview of render arrays, how to use three different cache types. We
used max-age for simple, time-based caching. Cache contexts provides a caching strategy
based on a variety of dynamic conditions. The tags cache type lets us invalidate caches based on
the activity on other entities or full control via custom tag names. Finally, we used “cache keys”
to protect against other cache invalidation in a render array tree.

This is it! I hope you enjoyed this tutorial! Stay tuned for more!

You might also like