Using Magento Enterprise Edition full page cache at your own controller action

Magento Full Page CacheAs a number one open source eCommerce platform with several years of development behind it, I'm sure you'll agree that Magento is one of the most complex pieces of PHP software ever created. Logical consequence of complexity Magento brings to the table is relatively low performance when compared to custom made eCommerce solutions. In order to improve this situation, Magento stacked several layers of caching one on top of another, with full page cache being the topmost layer, and one that's exclusive to Magento Enterprise Edition. Having power of full page cache nearby is great, but I must point out that by creating custom controller action in Magento you don't automatically benefit from performance boost FPC offers. In this article I'll outline how Magento Enterprise Edition full page cache works, and what you need to do in order to take advantage of features it provides at your custom controller action.

First things first, which controller actions does Enterprise_PageCache handle out the box? The answer to this question is that Magento Enterprise Edition full page cache processor comes with only four subprocessors, and by default handles only 404, CMS, category view and product view pages. This is obvious right from Enterprise_PageCache config.xml, relevant code excerpt is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- app/code/core/Enterprise/PageCache/etc/config.xml -->
 
<config>
 
    <!-- other xml -->
 
    <frontend>
 
        <!-- other xml -->
 
        <cache>
            <requests>
                <_no_route>enterprise_pagecache/processor_noroute</_no_route>
                <cms>enterprise_pagecache/processor_default</cms>
                <catalog>
                    <category>
                        <view>enterprise_pagecache/processor_category</view>
                    </category>
                </catalog>
                <catalog>
                    <product>
                        <view>enterprise_pagecache/processor_product</view>
                    </product>
                </catalog>
            </requests>
        </cache>
    </frontend>
 
    <!-- other xml -->
 
</config>

The advantage of using modular framework like Magento is that nothing prevents you from reusing built in subprocessors with your own controller action, or from writing your own subprocessor designed to handle your controller action's content.

Another important question, at which point Magento decides whether full page cache is capable of serving particular request? In order to provide performance boost, this magic obviously happens very early during Magento initialization, but also late enough to ensure usable environment capable of providing information about current request. This point is actually built into Magento Community Edition, something that gives indication that custom full page cache implementation is possible (several of them exist at Magento Connect). Here's relevant code fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
// app/code/core/Mage/Core/Model/App.php 
class Mage_Core_Model_App
{
    /* Other code */
 
    /**
     * Run application. Run process responsible for request processing and sending response.
     * List of supported parameters:
     *  scope_code - code of default scope (website/store_group/store code)
     *  scope_type - type of default scope (website/group/store)
     *  options    - configuration options
     *
     * @param  array $params application run parameters
     * @return Mage_Core_Model_App
     */
    public function run($params)
    {
        $options = isset($params['options']) ? $params['options'] : array();
        $this->baseInit($options);
        Mage::register('application_params', $params);
 
        if ($this->_cache->processRequest()) {
            $this->getResponse()->sendResponse();
        } else {
            $this->_initModules();
            $this->loadAreaPart(Mage_Core_Model_App_Area::AREA_GLOBAL, Mage_Core_Model_App_Area::PART_EVENTS);
 
            if ($this->_config->isLocalConfigLoaded()) {
                $scopeCode = isset($params['scope_code']) ? $params['scope_code'] : '';
                $scopeType = isset($params['scope_type']) ? $params['scope_type'] : 'store';
                $this->_initCurrentStore($scopeCode, $scopeType);
                $this->_initRequest();
                Mage_Core_Model_Resource_Setup::applyAllDataUpdates();
            }
 
            $this->getFrontController()->dispatch();
        }
        return $this;
    }
 
    /* Other code */
 
}

As you can see from previous code listing, Mage_Core_Model_App::run() contains very important if statement (line #24) calling Mage_Core_Model_Cache::processRequest(), and when condition of that if statement evaluates to true, this means one of the cache processor/subrocessor combination is able to handle current request. If this condition evaluates to false, Magento is loaded as usual but the page content get's grabbed and saved by Enterprise_PageCache_Model_Observer::cacheResponse() being triggered on controller_front_send_response_before.

So the main question this article is designed to answer is as follows. Where's the connection between that important if clause deciding on ability of FPC to process current request and code that saves body contents at controller_front_send_response_before event? Knowing answer to this question gives basic outline of how the Magento Enterprise Edition full page cache operates. For this we need to dig a little deeper, so follow me.

Saving page content into full page cache storage

I'll explain the observer triggered code first, so let's imagine that our magic if statement evaluated to false since no processors were able to handle current request, and that Magento had to load all of it's components (some of them potentially from some lower cache layer). In this case Enterprise_PageCache_Model_Observer::cacheResponse() gets triggered just before sending the response in order to pass cached content to full page cache storage backend. This process includes generating unique cache id used on subsequent page loads to identify cache content and to match saved data to request at hand. When it comes to Enterprise_PageCache, that's exactly where mentioned subprocessor classes come into play. As mentioned at the beginning of this article, Magento includes four subprocessors designed to handle four distinct requests types, and these classes contain two important functions that calculate unique cache id. These functions are getPageIdInApp() called from observer when saving full page cache data and getPageIdWithoutApp() called from Mage_Core_Model_App::run() when pulling data from full page cache storage. Here are the implementations taken from Enterprise_PageCache_Model_Processor_Default class serving as base class for all Enterprise_PageCache subprocessors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
// app/code/core/Enterprise/PageCache/Model/Processor/Default.php
 
/*
 * Function bodies are intentionally empty. Please refer to your copy of 
 * Magento Enterprise Edition source code.
 */
class Enterprise_PageCache_Model_Processor_Default
{
   /* Other code */
 
    public function getPageIdInApp(Enterprise_PageCache_Model_Processor $processor)
    {
       /*
        * Function bodies are intentionally empty. Please refer to your copy of 
        * Magento Enterprise Edition source code.
        */
    }
 
    public function getPageIdWithoutApp(Enterprise_PageCache_Model_Processor $processor)
    {
       /*
        * Function bodies are intentionally empty. Please refer to your copy of 
        * Magento Enterprise Edition source code.
        */
    }
 
    /* Other code */
}

As you can see, with default subprocessor class unique cache data identifier depends only on current request identifier (basically non query parameters part of the URL) and hash of current request query parameters, so that each URL gets saved to full page cache separately. For more complex subprocessor classes this isn't enough, so for example, Enterprise_PageCache_Model_Processor_Category subprocessor designed to handle category view pages has to take session data into an account when calculating unique cache id, and things get a bit more complicated. Reason for this is that not all of the required information is available that early in the load process when Magento decides on handling the request trough full page cache or not. To circumvent this limitation Magento usually stores this kind of information in cookies designed to get picked up on subsequent page loads.

So the basic idea is for Magento to save full page cache data under unique cache id derived from identifier returned by getPageIdInApp() executed on matching subprocessor class right before sending the response. Once Magento succeeds in matching the unique cache id derived from identifier returned by getPageIdWithoutApp() on the same subprocessor class, instead of processing the request as usual, matching data is returned as response on every subsequent page load. This tells us that in order to benefit from full page cache, we must ensure that both of these functions return the same output on the same request, and the definition of same request is what we define using our subprocessor class implementing these two methods.

Now to get back to the topic of this section, Enterprise_PageCache_Model_Observer::cacheResponse() calls Enterprise_PageCache_Model_Processor::processRequestResponse() what eventually triggers:

1
2
3
4
<?php 
// app/code/core/Enterprise/PageCache/Model/Processor.php
 
$cacheId = $this->prepareCacheId($processor->getPageIdInApp($this));

to obtain cache identifier in order to save data into full page cache storage.

Pulling content out of full page cache storage

By tracing the Mage_Core_Model_Cache::processRequest() code, we see that it iterates over all request processors in order to find the one capable of serving the request. It's worth mentioning once again that Enterprise_PageCache bundled request processor is potentially only one of the full page cache processors available on Magento system. Knowing to uniquely identify every request is crucial to serving up to date content and providing performance boost at the same time. So by following the code execution flow we come to conclusion that Mage_Core_Model_Cache::processRequest() calls Enterprise_PageCache_Model_Processor::extractContent() what eventually triggers:

1
2
3
4
<?php 
// app/code/core/Enterprise/PageCache/Model/Processor.php
 
$cacheId = $this->prepareCacheId($subprocessor->getPageIdWithoutApp($this));

and this cache id is used to lookup data from full page cache storage.

So the conclusion is that both observer code used to pass cache data to full page cache storage, and early load code used to pull data from the same storage are trying to obtain the same unique identifier, in order to avoid doing full Magento request processing with goal of boosting performance.

Taking advantage of full page cache at your own controller action

In my last article I have explained process of adding layered navigation to custom controller action in Magento, full source code is available from my Inchoo_Sale extension GitHub repository page. If you intend to use that code with Magento Enterprise Edition, it's beneficial to add your custom controller action to full page cache. Usually this would include creating custom full page cache subprocessor class implementing both getPageIdInApp() and getPageIdWithoutApp(), and registering your custom subprocessors with Enterprise_PageCache trough config.xml. Since custom controller action from my previous article mimics Magento category in order to create up to date sale area of the site, in this case we can simply register built in category page subprocessor class with our own controller action, and things will work beautifully.

In order to accommodate dynamic blocks like related products or global messages, Enterprise_PageCache goes even further and implements features like full page cache holes, something that definitely deserves an article of its own. For now here's an excerpt from config.xml to get you started with declaring Enterprise_PageCache subrocessor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- app/code/community/Inchoo/Sale/etc/config.xml -->
 
<config>
 
    <!-- other xml -->
 
    <frontend>
 
        <!-- other xml -->
 
        <cache>
            <requests>
                <inchoo_sale>
                    <index>
                        <view>enterprise_pagecache/processor_category</view>
                    </index>
                </catalog>
            </requests>
        </cache>
    </frontend>
 
    <!-- other xml -->
 
</config>

I believe this is something every Magento developer should be aware before diving into Magento Enterprise Edition, because it'll definitely save you from countless hours of debugging. Enjoy!

DevGenii

E-commerce is a breeze with Magento Certified Developer Plus & Zend Certified PHP Engineer nearby. Get in touch!

2 thoughts on “Using Magento Enterprise Edition full page cache at your own controller action

Leave a Reply

Your email address will not be published. Required fields are marked *