Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 373
novak Avatar asked Feb 02 '12 21:02

novak


People also ask

Can you cache dynamic content?

Unlike static content, dynamic content is different for each user, meaning it cannot be served to multiple users and is difficult to cache. However, caching dynamic content is possible with the right technology.

What is dynamic page cache?

Dynamic Page Cache overview Internal Dynamic Page Cache is a Drupal 8 core module. It caches pages for any user, handling dynamic content correctly.

What is full page cache?

"Full page cache" means that the entire HTML output for the page will be cached. Subsequent requests for the page will return the cached HTML instead of trying to process and re-build the page, thus returning a response to the browser much faster.

What is dynamic blocks in Magento 2?

A descriptive name that identifies the dynamic block in the Admin. Identifies the place in the standard page layout where the dynamic block is placed. Options: Content Area - Places the dynamic block in the main content area of the page.


3 Answers

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.

like image 54
Vinai Avatar answered Oct 20 '22 21:10

Vinai


In general, there are two approaches you can use if you want to personalise a page which is intended to be stored in a full page cache.

  1. If your reverse proxy supports it, you can use ESI (Edge Side Includes) and mark up your template appropriately. ESI allows you to insert a marker in your generated HTML where the personalised content should go, then your proxy will request just the personalised content from your app server's appropriate controller path when required. If you're using Varnish, ESI is available to use. The Lightspeed extension for Magento has a feature called "Hole Punching" which does a similar thing.
  2. If ESI or Hole Punching isn't available to you, then the other option is to allow the main page to be cached in your full page cache, and use a bit of javascript to make a separate Ajax request and fetch the information you need.
like image 30
Jim OHalloran Avatar answered Oct 20 '22 22:10

Jim OHalloran


I really appreciate the answer by Vinai. In addition, I would sugggest a FPC extension by Gordon Lesti that supports holepunching. You can get it over here

For instructions on how holepunching works with this extension, I suggest you to visit this page

I hope it helps someone who is not so much familiar with the concepts.

like image 1
Harit Avatar answered Oct 20 '22 22:10

Harit