Fragmented Thought

Caching Drupal Renderable Arrays

By

Published

Lance Gliser

Heads up! This content is more than six months old. Take some time to verify everything still works as expected.

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.

<p>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.</p>
<?php
$content['my_content'] = array(
  '#cache' => array(
    'cid' => 'my_module_data',
    'bin' => 'cache',
    'expire' => time() + 360,
  ),
  // Other element properties go here...
);
?>
<p>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.</p>

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. Anything 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.

/** * Implemenation of hook_node_view $view_mode = full * Renders child menu items of this node as full teasers. */ function custom_node_view_full($node, $view_mode, $langcode){ // Declare that we will be adding content $node->content['children'] = array( '#theme' => 'item_list', // Define what the theme function we'll use is '#items' => array(), // A property required by the theme function '#weight' => 99, // Defines where in the content this item appears '#attributes' => array( // Extra html attributes to be added to the final element 'class' => array('children', 'nodes') // Html classes ), // This is the key. You tell Drupal HOW to build the content to cache using #pre_render '#pre_render' => array('custom_node_view_full_children_prerender'), // Alert the render section that this is a cachable element '#cache' => array( // These 'keys' will be used to create a cache cid similar to: custom:content:###:children:full 'keys' => array('custom', 'content', $node->nid, 'children', 'full'), // Any valid cache constants or second based intervals are acceptable 'expire' => CACHE_TEMPORARY, ), ); } /** * Prerender function for the listing_page's #content['children'] element * @param array $element * @return array */ function custom_node_view_full_children_prerender($element){ // Prepare a place to keep children of this node $all_children = custom_get_page_menu_children(); // Example function, does not exist. $accessable_children = array(); if( !empty($all_children) ){ foreach($all_children as $child){ // Skip restricted items if( !$child['link']['access'] ){ continue; } $accessable_children[] = $child['link']; } } // Render them foreach($accessable_children as $child){ // Attempt the get the preview version of nodes to display $child_node = NULL; if( strpos($child['link_path'], 'node/') === 0 ){ $pieces = explode('/', $child['link_path']); $nid = $pieces[1]; if( !empty($nid) && is_numeric($nid) ){ $child_node = node_load($nid); } } // Add to the output list if( !empty($child_node) ){ // If we have a node, build a teaser $child_content = node_view($child_node, 'teaser'); $element['#items'][] = render($child_content); } else { // Otherwise output the title we do have $element['#items'][] = l($child['link_title'], $child['link_path']); } } return $element; }

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 prerender function, you'll have no effect on the output. Remember this, because there's no good reason I can think of to make it simple...

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