How to cache render arrays in Drupal 7

Author: Rolando Payan Mosqueda

 

Drupal 7 includes several features, including render arrays. If you are not familiar with render arrays,  the documentation at drupal.org  or  this article in Spanish  will explain very well what they are, why to use them and how to use them.

One of the specific features of render arrays that caught my attention was when I first saw the #cache property. This allows you to cache individual elements of your render arrays using Drupal's caching system, so for example, if you have some code that takes a long time to execute to display some content, it can be cached and later retrieved, saving valuable seconds or milliseconds when generating our page.

An example

So how can we use it? Here we can see an example, it is similar to the one that exists in render_example.module in the  Examples module :

Note that we are returning an unrendered array. This is a function that will be executed when visiting a page, so the theme layer is expected to pass this array through drupal_render() after other modules or themes have a chance to modify it. At other times we could easily call drupal_render() ourselves and we could instead:

return $page_array;

replace it with:

return drupal_render($page_array);

If we do this, our render array becomes meaningless, since it could not be modified by other modules or themes.

The array renderer is very simple. The #markup property is the content of the array, which will be cached. The #cache property tells Drupal how we want it to be cached.

 

'keys' => array('render_example', 'cache', 'demonstration'),

The key “key” is an array of string that together must be unique, and is the identifier of our content, this will be concatenated, separated by colons and used as the id of our cache, in our case the result would be: render_example:cache :demonstration

 

'bin' => 'cache',

The “bin” key is the table where we will store our content. Here we use the generic cache table, but you can also use your own custom table. Creating a custom table is easy. In our .install file (mymodule.install) add:

Change “mymodule” to the name of your module. With this example you could configure the key “bin” with the value “cache_mymodule”, example:

'bin' => 'cache_mymodule',

 

'expire' => time() + $interval,

The “expire” key is a timestamp that tells Drupal when our cached content will no longer be valid and will need to be rebuilt. It is worth noting that caches that expire will be cleared the next time the cron is run, so you may encounter the case where you are viewing cached content and its time has already expired.

'granularity' => DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE,

The “granularity” key indicates if and in what situations this content should be cached. The content may be different depending on the user viewing it, on the page it appears, etc. For example, we don't want anonymous users to see content that has been cached for an administrator. This allows us to store separate cache versions by role to avoid that situation. These values ​​can be used:

  • DRUPAL_CACHE_PER_PAGE

  • DRUPAL_CACHE_PER_ROLE

  • DRUPAL_CACHE_PER_USER

  • DRUPAL_CACHE_GLOBAL

  • DRUPAL_CACHE_CUSTOM

  • DRUPAL_NO_CACHE

A problem

So it seems easy. We were able to add caching to our render array with just a few lines. But there is a problem. You've seen? Maybe it's easier to see if we rewrite our example.

We moved our content to a separate function and added drupal_set_message(). Why did we make them? If we reload our page several times you will see the problem. The render array is caching our content. You can tell this because every time you refresh the page the time remains the same as when it was first loaded. But the Drupal message does show the updated time every time the page is reloaded. Wait, why does this message appear on every page reload? Wasn't the objective of caching our expensive_stuff() function so that it wouldn't be executed and thus save time loading our page? If the function runs all the time, our code for caching the content is meaningless. To clarify this we need a little more work on our #cache property.

This behavior should be obvious in retrospect. Render arrays are just regular arrays, formatted in a particular way and passed to drupal_render(). drupal_render() doesn't have a magical ability to come in and prevent our expensive_stuff() function from running if it finds a cached version. So how do we make this work?

A solution

The solution is the #pre_render property. pre_render allows us to pass it an array with the names of functions that will be called just before the array is rendered. The key here is that drupal_render() calls these functions after it has checked if a cached version of the element exists. If it finds a cached version then those functions are not called. Here is our example reworked to use #pre_render, so that #cache is still useful:

Note that the #markup property is now an empty string in the render array. Technically it doesn't have to be there. Instead we add the content to #markup via the #pre_render function which is then responsible for calling our expensive_stuff() function. Reloading our example page several times we must realize that the expensive_stuff() function is only executed the first time we load our page and then each time the cache expires.

And that's all, I hope you found this article interesting.