Caching Drupal Renderable Arrays

Love getting to learn something new. Going through some tutorials today, I realized there's an 'easy' hook to enable caching of the renderable arrays I never knew about. What exactly is a renderable array? ... Maybe the easiest way you can think of it is structured data that needs to be transformed into html. Previously I'd been using cache_set and cache_get, but this is quicker, and a bit more flexible. You’d generally see modules building something like this when creating content inside a view using hook_node_view. But, knowing the caching option exists, I might start pushing it down to the theme layer more often.

Theoretical Example

There's an initial breakdown of this technique I'll copy verbatim from lullabot.

In Drupal 7, "renderable arrays" are used extensively when building the contents of each page for display. Modules can define page elements like blocks, tables, forms, and even nodes as structured arrays; when the time comes to render the page to HTML, Drupal automatically uses thedrupal_render() function to process them, calling the theme layer and other helper functions automatically. Some complex page elements, though, can take quite a bit of time to render into HTML. By adding a special #cache property onto the renderable element, you can instruct thedrupal_render() function to cache and reuse the rendered HTML each time the page element is built.

  1. $content['my_content'] = array(
  2.   '#cache' => array(
  3.     'cid' => 'my_module_data',
  4.     'bin' => 'cache',
  5.     'expire' => time() + 360,
  6.   ),
  7.   // Other element properties go here...
  8. );

The #cache property contains a list of values that mirror the parameters you would pass to the cache_get() andcache_set() if you were calling them manually. For more information on how caching of renderable elements works, check out the detailed documentation for the drupal_render() function on api.drupal.org.

Working Example

But, that doesn't really fully explain it the usage. See, a novice might look at that think they are done. But there's a gotcha here.. Drupal can't magically stop your function executing. If you tell it there's a cachable array, but then immediately spend your processor cycles doing the expensive operation, you actually make the process worse. Maybe a better example than lullabot's would be something like this:

Our goal is to access this page's menu item, and output content defined as submenu items. For identifiable nodes as immediate children, we want to render a preview. Any thing else can be output as a text only link. We'll start by using node_view to declare the content. Then use the #pre_render property of the array to call our expensive function that does the actual processing.

  1. /**
  2.  * Implemenation of hook_node_view $view_mode = full
  3.  * Renders child menu items of this node as full teasers.
  4.  */
  5. function custom_node_view_full($node, $view_mode, $langcode){
  6.   // Declare that we will be adding content
  7.   $node->content['children'] = array(
  8.     '#theme' => 'item_list', // Define what the theme function we'll use is
  9.     '#items' => array(), // A property required by the theme function
  10.     '#weight' => 99, // Defines where in the content this item appears
  11.     '#attributes' => array( // Extra html attributes to be added to the final element
  12.           'class' => array('children', 'nodes') // Html classes
  13.     ),
  14.     // This is the key. You tell Drupal HOW to build the content to cache using #pre_render
  15.     '#pre_render' => array('custom_node_view_full_children_prerender'),
  16.     // Alert the render section that this is a cachable element
  17.     '#cache' => array(
  18.       // These 'keys' will be used to create a cache cid similar to: custom:content:###:children:full
  19.       'keys' => array('custom', 'content', $node->nid, 'children', 'full'),
  20.       // Any valid cache constants or second based intervals are acceptable
  21.       'expire' => CACHE_TEMPORARY,
  22.     ),
  23.   );
  24. }
  25.  
  26. /**
  27.  * Prerender function for the listing_page's #content['children'] element
  28.  * @param array $element
  29.  * @return array
  30.  */
  31. function custom_node_view_full_children_prerender($element){
  32.   // Prepare a place to keep children of this node
  33.   $all_children = custom_get_page_menu_children(); // Example function, does not exist.
  34.   $accessable_children = array();
  35.   if( !empty($all_children) ){
  36.     foreach($all_children as $child){
  37.       // Skip restricted items
  38.       if( !$child['link']['access'] ){
  39.         continue;
  40.       }
  41.       $accessable_children[] = $child['link'];
  42.     }
  43.   }
  44.   // Render them
  45.   foreach($accessable_children as $child){
  46.     // Attempt the get the preview version of nodes to display
  47.     $child_node = NULL;
  48.     if( strpos($child['link_path'], 'node/') === 0 ){
  49.       $pieces = explode('/', $child['link_path']);
  50.       $nid = $pieces[1];
  51.       if( !empty($nid) && is_numeric($nid) ){
  52.         $child_node = node_load($nid);
  53.       }
  54.     }
  55.     // Add to the output list
  56.     if( !empty($child_node) ){
  57.       // If we have a node, build a teaser
  58.       $child_content = node_view($child_node, 'teaser');
  59.       $element['#items'][] = render($child_content);
  60.     } else {
  61.       // Otherwise output the title we do have
  62.       $element['#items'][] = l($child['link_title'], $child['link_path']);
  63.     }
  64.   }
  65.  
  66.   return $element;
  67. }

One final caveat

Sadly, we're not quite done yet. There's one more pitfall you might not easy find... When you create a renderable array, many times it comes down to '#markup' properties. And that's the part Drupal doesn't handle well. It turns out if you declared a renderable array cachable, but only adjust the '#markup' property of $element in your pre_render function, you'll have no affect on the output. Remember this, because there's no good reason I can think of to make it simple...

  1. /**
  2.  * Prerender function example for #markup
  3.  */
  4. function custom_some_element_pre_render($element){
  5.   // Adjusting $element['#markup'] seems logic, but has no affect
  6.   // You need to instead set $element['#children'] direclty
  7.   $element['#children'] = '<p>Working html</p>';
  8.   return $element;
  9. }

Comments

I decided at one point that I'd use this technique render multiple teaser and embedded view modes of nodes. Turns out, that's not such a good idea. Because the cache builds while you are logged in but displays that way to anonymous users, you'll find nasty little surprises. A tell tale sign of this kind of trouble would be the contextual links for edit and delete displaying as block level content for anonymous users.

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.