Fork me on GitHub

grimhappy.com

menu callback cache

| permalink

Drupal supports page caching for anonymous users which can be a very important performance boost for your site. It works by taking the output of a page and placing it in the page_cache table. As you can see from the below picture, you have three configuration options for page caching:

  • Caching Mode - Aggressive or Normal. Normal caching will cache pages with no crazy side effects. Aggressive caching will go a step further and ignore running Drupal's hook_boot and hook_exit. This will be faster but could result in some really spooky behavior depending on what modules you have installed and what they are doing during boot and exit.
  • Minimum cache lifetime - This is a quick way of saying "Don't rebuild any cached pages until at least X minutes have passed"
  • Page compression - Drupal can also compress pages that it caches to optimize bandwidth. Most servers already support compression and if that the case this should be disabled.

Drupal Page Cache

Better Faster Stronger Page Caching

This is nice and all, but what if you need more control over which pages are cached? What if you have an article content type that you want to cache for 5 minutes, but you don't want to cache any other content types? What if you want to cache page X for 10 minutes unless a user has a specific role? Drupal's out of the box page caching cannot solve these problems. Menu Callback Cache to the rescue!

Drupal's menu system is very flexible. It works like URL routers in most development frameworks. You specify a URL pattern, and then a callback to execute for that pattern. By default, nodes in Drupal have the all too familiar URL structure node/[nid] where nid is the ID of the node you are viewing. We can see how this is defined in Drupal core:

  :::php
  $items['node/%node'] = array(
    'title callback'   => 'node_page_title',
    'title arguments'  => array(1),
    'page callback'    => 'node_page_view',
    'page arguments'   => array(1),
    'access callback'  => 'node_access',
    'access arguments' => array('view', 1),
    'type'             => MENU_CALLBACK
  );

The Menu Callback Cache (MCC) module works by augmenting Drupal's menu system. It provides 3 new keys that you can specify in hook_menu:

  • cache
  • cache key callback
  • cache max age

cache

This is the only required key to enable menu callback caching. If set to TRUE, MCC will cache the menu item's output for 5 minutes globally for all users.

cache key callback

By default, MCC generates its own cache key based off of the name of the page callback and a hash of the arguments. If, however, you would like to specify your own cache key, you can implement a cache key callback. It accepts the same arguments as the page callback and must return a string. If you want to cache a menu item conditionally, you can throw a DoNotCacheException in the cache key callback which will bypass MCC.

cache max age

Sets the max age of the cache menu item, defaults to 5 minutes.

Examples!

Here is an example of the most basic usage, caching a menu callback for 5 minutes:

  :::php
  /**
   * Implementation of hook_menu().
   */
  function my_module_menu() {
    $items = array();

    $items['foo'] = array(
      'title' => 'Foo', 
      'description' => 'Foooooooooooooo.', 
      'page callback' => 'my_module_foo', 
      'access arguments' => array('access content'), 
      'type' => MENU_NORMAL_ITEM,
      'cache' => MENU_CALLBACK_CACHE_PER_USER,
    );

    return $items;
  }

  /**
   * Page callback for 'foo/'
   */
  function my_module_foo() {
    return "This is some content, yo.";
  }

Now let's see how we would use MCC to solve our earlier questions:

What if I have an article content type that I want to cache for 5 minutes, but I don't want to cache any other content types?

  :::php
  /**
   *  Implementation of hook_menu_alter().
   */
  function my_module_menu_alter(&$items) {
    $items['node/%node']['cache'] = MENU_CALLBACK_CACHE_PER_USER;
    $items['node/%node']['cache max age'] = 60 * 5;
    $items['node/%node']['cache key callback'] = 'my_module_cache_key_callback';
  }

  /**
   *  Cache key callback for path node/%node.
   */
  function my_module_cache_key_callback($node) {
    // Set the cache key if this node is of type 'page'.
    if ($node->type == 'article') {
      return __FUNCTION__ . '-' . $node->nid;
    }
    // Don't cache nodes that aren't type 'article'.
    else {
      throw new DoNotCacheException();
    }
  }

In the previous example I implemented a cache key callback. This means I can clear that cache programmatically if ever I need to:

  :::php
  /**
   * Implementation of hook_nodeapi.
   */
  function my_module_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
    switch ($op) {
      case 'update':
        // Clear the cached menu callback when we update the node.
        cache_clear_all('my_module_cache_key_callback-' . $node->nid);
        break;
    }
  }

What if I want to cache page X for 10 minutes unless the user has a specific role?

  :::php
  /**
   * Implementation of hook_menu().
   */
  function my_module_menu() {
    $items = array();

    $items['x'] = array(
      'title' => 'Page X', 
      'description' => 'SeXy.', 
      'page callback' => 'my_module_x', 
      'access arguments' => array('access content'), 
      'type' => MENU_NORMAL_ITEM,
      'cache' => MENU_CALLBACK_CACHE_GLOBAL,
      'cache key callback' => 'my_module_x_cache_key_callback',
      'cache max age' => 60 * 10
    );

    return $items;
  }

  /**
   * Cache key callback for page x.
   */
  function my_module_x_cache_key_callback() {
    global $user;

    // Don't let editors see cached page.
    if (in_array('editor', array_values($user->roles))) {
      throw new DoNotCacheException();
    }

    return 'cache-x';
  }

  /**
   * Page callback for 'x/'
   */
  function my_module_x() {
    return "This is some content, yo.";
  }

This module is currently in use on a very high traffic website to provide some very helpful page caching. When coupled with Memcached, it can be a very useful tool in helping to build high performance Drupal modules. I am currently working on another post that describes several tips and tricks for developing high performance modules.

hello world

| permalink

Yes this is my 5th attempt to start blogging. Yes this will probably be my 5th failure. But you know what Thomas Edison did when his first attempt at the lightbulb failed? He patented it. Apparently patenting failure is a valid approach. I've already submitted my blog to the US Patent office. I think you already owe me like 47 cents.

The truth is, I was looking at the code for Brian Leroux's wtfjs and really liked the idea of using git as a sort of publishing system. wtfjs runs on node.js, however, and I couldn't figure out how to get node running on Media Templs (gs). So I did the next best thing. I (very loosely) ported the idea to PHP. You can grab the code here. It is a constant work in progress, but I've been very pleased so far.

Creating a new post is as simple as creating a new file in the posts directory following the naming scheme: YYYY-MM-DD-my-post-title.md. Everything uses markdown for formatting. Pages are created similarly by adding a new file to the pages directory following the naming scheme page-name.md.

You can add your own behavior very easily too! Just pop open index.php and add a new url and callback:

  :::php
  get('^whatsup$', function() {
    if (rand(0, 1)) {
      echo 'Not much bra';
    }
    else {
      Response::error404();
    }
  });

So there you have it! Please use it! Tell me if something doesn't work!