Did you ever try to make a block that is cached by default uncacheable? Let's play a little bit with caches and layout xml and you might learn a trick or two along the way...

First of all: Why would you want to make a block uncacheable?! - Just because you can :) And here's an example:

Let's look at the footer block (Mage_Page_Block_Html_Footer) - one of the few blocks that are cached by default in Magento. Btw, you can use Aoe_TemplateHints for a nice graphical representation of what's cached and what's not:

Here's where the caching is configured:

class Mage_Page_Block_Html_Footer extends Mage_Core_Block_Template
{

    protected function _construct()
    {
        $this->addData(array('cache_lifetime' => false));
        $this->addCacheTag(array(
            Mage_Core_Model_Store::CACHE_TAG,
            Mage_Cms_Model_Block::CACHE_TAG
        ));
    }

    [...]
}

Let's say you put something inside the footer that's customer specific (e.g. "products you might like"). Since the footer is cached everything that's inside that footer is implicitly caches (Aoe_TemplateHints shows this with a yellow border). So you wouldn't be able to have customer specific content unless

  1. don't nest that block inside the footer in the first place
  2. you "tell" the footer block about the different versions you want by adding the customer id to the cache key
  3. you disable caching for the footer block and cache the children individually.

While 1.) and 2.) might be smarter let's proceed with option 3.) for now and try to make Magento not cache the footer block in the first place...

Block Caching 101

As you might know block caching in Magento is controlled via following methods (checkout Mage_Core_Block_Abstract)

  • getCacheKey() (by default generating a key using getCacheKeyInfo())
  • getCacheLifetime()
  • getCacheTags()

But it's really getCacheLifetime() that controls if a block is cached or not and if so how long it is cached.

Here are the - not so intuitive - rules:

$x = $this->getCacheLifetime() What happens
null* no caching
0 / false cache forever
> 0 cache for $x seconds

* don't confuse this with Zend_Cache_Backend_Interface where a lifetime of null actually means forever instead of no caching.

What's not working

While overriding the lifetime like this works perfectly fine (in this case the footer will be cached for a day instead of forever):

<reference name="footer">
    <action method="setCacheLifetime"><lifetime>86400</lifetime></action>
</reference>

adding null (or any other non-integerish value) does not work:

<reference name="footer">
    <action method="setCacheLifetime"><lifetime>null</lifetime></action>
</reference>

Technically it's up to the cache backend implementation, but most of the backends will interpret this as a lifetime of 0 and make the cache expire right away. Well, technically this is exactly what you want, but besides the fact that this is unreliable (since different cache backends might handle this differently) and exceptionally ugly you end up writing data into the cache (which might be slow) that is expired right away (which also will lead to cache fragmentation over time...)

Leaving the parameter empty also does not work (since then for some reason a Mage_Core_Model_Layout_Element is passed as an argument resulting in the same undefined situation we had with strings)

<reference name="footer">
    <action method="setCacheLifetime"><lifetime></lifetime></action>
</reference>

Option 1: Rewrite

So how do we handle this problem? The obvious solution would be to rewrite the footer class overwriting the _construct() method to prevent the cache_lifetime to be set in the first place:

class MyModule_Page_Block_Html_Footer extends Mage_Page_Block_Html_Footer
{

    protected function _construct()
    {
        // skip setting the cache_filetime to prevent caching
        // $this->addData(array('cache_lifetime' => false));
        // $this->addCacheTag(array(
        //     Mage_Core_Model_Store::CACHE_TAG,
        //     Mage_Cms_Model_Block::CACHE_TAG
        // ));
    }

    [...]
}

Again, this works fine, but it just feels wrong to rewrite a class (with another almost empty class) to do this.

Option 2: Layout XML helper

Now, let's get to the more exciting stuff: Let's do this via layout xml, but since we already know that <action method="setCacheLifetime"><lifetime>null</lifetime></action> will result in the cache lifetime being a string 'null' (instead of actually being null) how do we get the correct null value into the cache lifetime property?

Let's use a simple helper:

class My_Module_Helper_Data extends Mage_Core_Helper_Abstract {

    public function returnNull() 
    {
        return null;
    } 
}

And set the value in layout xml like this:

<reference name="footer">
    <action method="setCacheLifetime"><lifetime helper="mymodule/returnNull" /></action>
</reference>

With this trick the helper is being called and returns a real null that is then being passed as an argument to setCacheLifetime(). This way we can bypass layout xml's restriction of not being able to express null values.

Please note that the usage of helpers - while looking similar to Magento class names - work differently: The part before '/' is your module where the data helper is implied and the part after the '/' is the method name in that data helper!

Option 3: JSON

So this is the solution that got me most excited since I didn't know about this feature in layout xml before today and since this isn't used anywhere in the Magento code I bet this is news to many of you too:

You can tell Magento which arguments should be parsed as JSON!

This works similar to the "translate" feature you might have seen before where in this case the content of the <label> and the <title> argument are translated before being passed to the addLink() method:

<action method="addLink" translate="label title" module="customer">
    <label>Log In</label>
    <url helper="customer/getLoginUrl"/>
    <title>Log In</title>
    <prepare/>
    <urlParams/>
    <position>1</position>
</action>

Here's an example of the JSON feature:

<action method="setComplexData" json="jsondata1 jsondata2">
    <jsondata1><![CDATA[{"firstname": "John", "lastname": "Doe"}]]></jsondata1>
    <regularvalue>42</regularvalue>
    <jsondata1><![CDATA[{"zip": "94015", "state": "California"}]]></jsondata1>
</action>

This will result in a call like this:

setComplexData(
    object(stdClass)#1 (2) { 
        ["firstname"]=>string(4) "John"
        ["lastname"]=>string(3) "Doe"
    },
    '42',
    object(stdClass)#1 (2) { 
        ["zip"]=>string(5) "94015"
        ["state"]=>string(10) "California"
    }
)

And luckily according to the JSON specification not only objects are allowed values, but also strings, true, false and: null (and Mage::helper('core')->jsonDecode() does respect that)!

So this is why it works perfectly fine when you're trying to set a null value (or a real boolean false) to a method via layout xml:

<action method="setCacheLifetime" json="jsondata">
    <jsondata>null</jsondata>
</action>

Find out more about this feature by looking at Mage_Core_Model_Layout->_generateAction().

Option 4: Trivial solution

Ok, if you're still reading, here's the last, and probably most trivial option. And of course by far the most boring one:

<reference name="footer">
    <action method="setCacheLifetime"></action>
</reference>

(Note that there's no argument tag at all)

This works because conveniently Varien_Object->__call() takes care of missing arguments and replaces them with null which again luckily is exactly what we need here:

$result = $this->setData($key, isset($args[0]) ? $args[0] : null);

Comments

This website uses disqus for the commenting functionality. In order to protect your privacy comments are disabled by default.

Enable Comments