Professional Documents
Culture Documents
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!
Prerequisites
● Familiarity with custom module development.
● How to create a custom controller to process incoming requests.
● Some knowledge of render arrays.
● 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.
Now we should have an *.info.yml, *.routing.yml and our controller class.inally, let’s enable our
custom module:
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:
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”
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:
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:
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.
Sometimes, we only want to invalidate the cache based on a specific argument in the URL query. We
can do that too:
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…
If we visit:
http://your_drupal_site.test/d8_cache/contexts-param?this_is_another_query_param=value
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:
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:
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!
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'],
]
]
];
}
We get this:
Just a message!
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:
Just a message!
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:
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']
]
]
];
}
We will get:
Just a message!
Just a message!
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!