How do I include a dynamic block in the product page with full page caching turned on?

novak picture novak · Feb 2, 2012 · Viewed 20.9k times · Source

We would like to add a dynamic block to the product page. The problem is that the product page has full page caching (and we cannot turn that off due to speed issues). We want to display different information on each product page based on the logged in user's account, and it varies from product to product.

I created a separate block that has its own caching, but this displays the same block from the previous product page. I'm trying to alter it's caching method so it doesn't save the cache from the previous product page.

It works the first few times I go to the product pages, but then suddenly starts displaying a Magento error page that says, "The website encountered an error while retrieving http://www.mycompany.com/productpage.html.
It may be down for maintenance or configured incorrectly."

Here is what I've done so far.

I created app/code/local/MyCompany/MyModule/PageCache/etc/config.xml to add MyCompany_PageCache_Model.

Then I created the file that controls caching in app/code/local/MyCompany/MyModule/PageCache/Model/Container/MyFile.php
with these functions:

protected function _getCacheId()
{
    return 'CONSTANT_CACHE' . md5($this->_placeholder->getAttribute('cache_id'));
}

protected function _saveCache($data, $id, $tags = array(), $lifetime = null)
{
    return false;
}

protected function _renderBlock()
{
    $blockClass = $this->_placeholder->getAttribute('block');
    $template = $this->_placeholder->getAttribute('template');

    $block = new $blockClass;
    $block->setTemplate($template);
    $block->setLayout(Mage::app()->getLayout());
    return $block->toHtml();
}

I also created cache.xml under Catalog/etc with my placeholder for CONSTANT_CACHE.

Is the syntax above incorrect, or is there an easier way to do this?

Answer

Vinai picture Vinai · Feb 3, 2012

Overview

In order to answer I need to explain a little first. The Magento FPC process knows four states.

  1. Page in cache, no dynamic blocks
  2. Page in cache, dynamic blocks cached
  3. Page in cache, dynamic blocks not cached
  4. Page not in cache

State 1 and 2 are processed without the full Magento application being initialized. State 3 and 4 require the application to be initialized and routing to be processed. For that reason, aim to serve requests from state 1 and 2 if possible, otherwise you are losing a big part of the possible improvements of the FPC.

State 1

State 1 is boring from a developer point of view, nothing to do, so lets move on to...

State 2

In state 2 a page contains dynamic blocks. Right now, Magento has not been fully initialized.
The FPC processor loads a cached page and finds a placeholder for a dynamic block in it.
By analyzing the placeholder, the processor is able to identify the container class for the dynamic block, instantiates it, and calls applyWithoutApp($content) on it. (The name of the method refers to the fact that the Magento application hasn't been initialized so far). The container then tries to load the dynamic block contents from the block cache, using the cache key returned by the method $this->_getCacheId().
If a cache key is returned and a cache entry could be loaded, the container class replaces the placeholder in the $content with the cached block output and the FPC is done.
So far not much overhead has been produced.

State 3

So applyWithoutApp($content) in state 2 was unable to fetch and deliver the dynamic block content, so the block content needs to be generated, even though the rest of the page has been found in the FPC.
For this purpose the FPC module sets the request to pagecache/request/process, and the regular Magento application initialization and routing is followed.
This means a lot more overhead is produced then with state 2, even though it still is a bit better then a regular page load without the FPC, because e.g. the URL rewriting is skipped.
Finally the front controller and standard router delegate the request to the RequestController::processAction()method.
The method fetches the previously instantiated container class for the dynamic block, and calls applyInApp($content) on it.
This method runs $this->_renderBlock() to instantiate the real block class and return it's output. You already implemented this method according to your question. The FPC can now replace the placeholder with the block content and deliver the page.
One thing to be aware of is that this is not a regular product detail page request, so e.g. Mage::registry('current_product') is not available! Depending on your block implementation, this might influence the block level caching or content generation of the dynamic block. I suspect this might be where your problem stems from, but I'll get to a possible workaround a bit further down.

State 4

In this state the FPC didn't find a cache record for the requested page, so Magento generates the page as usual, e.g. the product detail page output is created by the Mage_Catalog_ProductController::viewAction().
All blocks that are configured to be dynamic, according to the cache.xml, are wrapped in placeholder tags.
The placeholder tags contain arguments, that are later passed to the container object for step 2 and 3. The only arguments that always are set are the container and the block class names. But almost always a cache_id and a template are set as well.
In the container class, these values can be accessed using $this->_placeholder->getAttribute('cache_id') (like you did in the _getCacheId() method of your container).

Even if you where glossing over most of this lengthy answer, this is where it might get interesting for you. If you need additional values to generate the blocks cache id or the block output, (e.g. the product id or the customer id), you can set these as arguments to the placeholder.

To do so you need to set them on the array returned by the block getCacheKeyInfo() method with a string as an array key. If you use a numeric array index they will not be set as arguments on the placeholder.

public function getCacheKeyInfo() {
    $info = parent::getCacheKeyInfo();
    $info['current_product_id'] = Mage::registry('current_product')->getId();
    $info['customer_id'] = Mage::getSingleton('customer/session')->getCustomerId();
    return $info;
}

These values are now accessible in the container class using $this->_placeholder->getAttribute('current_product_id').

Conclusion

You probably don't want to override _saveCache() in your container class to return false. Instead, include the customer id and product id in the string returned by _getCacheId(). That way each customer get's his own cache entry. Some overhead will be reduced because applyWithoutApp() can save and load the dynamic block from the cache (if a page is viewed twice by the same customer).

In _renderBlock() set the additional values you need in order for the block to be able to generate it's contents on it, e.g.

$block->setProductId($this->_placeholder->getAttribute('current_product_id'));

On the block side of things, including the product id and the customer id in the cache info array will ensure that each customer get's the correct output for the requested page, even when the block is cached.

I can't know for sure, (you haven't provided the block code), but I suspect the cache id you are using doesn't contain all the arguments it needs to uniquely map the cache record for the block to the right product.

Using the steps and knowing how to pass arguments to a dynamic block container it is possible to retain most of the FPC performance gain, even when creating custom dynamic blocks. I hope this information is enough for you to be able to track down the problem you are describing and fix it.