Mini-Tutorial – Hole-Punching Configurable Product Options

While working on a project for a client, I was challenged with the tasked of ensuring that size and colour combinations for a configurable product show up accurately. If we were working with Magento Community Edition this really wouldn’t be that challenging of an issue, but as with most of our clients we are working on the Enterprise Edition. This edition implements Full Page Caching, so after a page is generated once its outputs are saved and returned for subsequent calls when the cache is cleared or when the specific entries cache has expired. This presents a unique challenge when trying to serve pages with accurate data, but still take advantage of the caching features.

In Magento “Hole-Punching” refers to allowing dynamic content to be displayed on a page that is saved in cache. So essentially the idea is to have a portion or portions of the page like a hole or window through to the real-time data needed to display the relevant content. In the Magento Enterprise core, there are methods and means implemented to take care of this challenge, which are used on most pages already. Error Messages, the “Mini-Cart”, Related Products etc., all take advantage of the built in “Hole Punching” abilities.

Here is the solution that I came up with to tackle this. I am aware that there are certain scenarios that this may not account for (if all items are out of stock, the add to cart button may still appear, although this is just cosmetic). One caveat is you need to have the stock index set to “run frequently, or “update on save” to properly take advantage of this. Although it may not be perfect, I felt this was at least a good starting point and believed others may find it useful.

The Code:

Declare Our Module

First we need to declare our module (Please note I have chosen to put this module in the community code pool as I am distributing it, however if you were doing a specific customization for a client, you may choose to use the local code pool):

app/etc/modules/Demac_CustomOptionsRegex.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Demac_LiveConfigurables>
            <active>true</active>
            <codePool>community</codePool>
        </Demac_LiveConfigurables>
    </modules>
</config>

Create Out Modules config.xml File

In this we define our own block, model and add definition for a layout file for the frontend.

app/code/community/Demac/LiveConfigurables/etc/config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Demac_LiveConfigurables>
            <version>0.1.0</version>
        </Demac_LiveConfigurables>
    </modules>
    <global>
        <blocks>
            <demac_liveconfigurables>
                <class>Demac_LiveConfigurables_Block</class>
            </demac_liveconfigurables>
        </blocks>
        <models>
            <demac_liveconfigurables>
                <class>Demac_LiveConfigurables_Model</class>
            </demac_liveconfigurables>
        </models>
    </global>
    <frontend>
        <layout>
            <updates>
                <demac_liveconfigurables>
                    <file>demac_liveconfigurables.xml</file>
                </demac_liveconfigurables>
            </updates>
        </layout>
    </frontend>
</config>

Initiating cache.xml File to Add a Place Holder

Here is the cache.xml file. This is crucial as it declares the block, the name and the container that will be used to add a placeholder for the “Hole Punched” content. It is important to note that the name represents the name attribute given to the block in the modules layout xml. As well in this case the cache_lifetime is not important as we are not planning to save the cache as you will see in the container code.

app/code/community/Demac/LiveConfigurables/etc/cache.xml

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <placeholders>
        <demac_liveconfigurables_jsonconfig>
            <block>demac_liveconfigurables/jsonconfig</block>
            <name>live_json_config</name>
            <placeholder>live_json_config</placeholder>
            <container>Demac_LiveConfigurables_Model_Container_Jsonconfig</container>
            <cache_lifetime>1</cache_lifetime>
        </demac_liveconfigurables_jsonconfig>
    </placeholders>
</config>

The Jsonconfig Block

Here we have the Jsonconfig block, although this is not 100% needed as it is an empty class I have chosen to use this to allow for extra functionality in the future if needed and to be clear with the usage of the jsonconfig phtml file.

app/code/community/Demac/LiveConfigurables/Block/Jsonconfig.php

<?php
/**
 * Class Demac_LiveConfigurables_Block_Jsonconfig
 */
class Demac_LiveConfigurables_Block_Jsonconfig extends Mage_Catalog_Block_Product_View_Type_Configurable {

}

The Container File

Here is the container file I referenced earlier. This file controls how the “Hole Punched” content is handled and rendered, specifically note that we have overridden the _saveCache function in order to always use real-time data. As an alternative you could allow the cache to save, and have the content expire on a schedule to save some processing.
app/code/community/Demac/LiveConfigurables/Model/Container/Jsonconfig.php

<?php
/**
 * Class Demac_LiveConfigurables_Model_Container_Jsonconfig
 */
class Demac_LiveConfigurables_Model_Container_Jsonconfig extends Enterprise_PageCache_Model_Container_Abstract
{
    /**
     * Get cache identifier
     *
     * @return string
     */
    protected function _getCacheId()
    {
        return $this->_placeholder->getAttribute('cache_id');
    }

    /**
     * Render block content
     *
     * @return string
     */
    protected function _renderBlock()
    {
        $blockClass = $this->_placeholder->getAttribute('block');
        $template = $this->_placeholder->getAttribute('template');

        $block = new $blockClass;
        $block->setTemplate($template);

        $product = Mage::registry('product');
        
        //Check if $product exist in the registry as it is needed to render the output
        if (!($product && $product->getId()) && $this->_getProductId()) {
            $product = Mage::getModel('catalog/product')
                ->setStoreId(Mage::app()->getStore()->getId())
                ->load($this->_getProductId());

            if ($product && $product->getId()) {
                Mage::register('product', $product);
            }
        }

        return $block->toHtml();
    }

    /**
     * @param string $data
     * @param string $id
     * @param array $tags
     * @param null $lifetime
     * @return bool|Enterprise_PageCache_Model_Container_Abstract
     */
    protected function _saveCache($data, $id, $tags = array(), $lifetime = null)
    {
        return false;
    }

}

Code for the Layout File

This is the code for the layout file, in it we specifically target configurable product pages, and set it to use our custom templates.

app/design/frontend/base/default/layout/demac_liveconfigurables.xml

<?xml version="1.0"?>
<layout version="0.1.0">
    <PRODUCT_TYPE_configurable>
        <reference name="product.info.options.configurable">
            <action method="setTemplate">
                <template>demac/liveconfigurables/configurable.phtml</template>
            </action>
            <block type="demac_liveconfigurables/jsonconfig" name="live_json_config"
                   template="demac/liveconfigurables/jsonconfig.phtml"/>
        </reference>
    </PRODUCT_TYPE_configurable>
</layout>

configurable.phtml…Again

This is basically identical to the default configurable.phtml file with on change, calling our “Hole Punched” block via getChildHtml(). If you had a custom theme you could simply take that line and add it your template in place of the default javascript which gets the configurable options.

app/design/frontend/base/default/template/demac/liveconfigurables/configurable.phtml

<?php
$_product    = $this->getProduct();
$_attributes = Mage::helper('core')->decorateArray($this->getAllowAttributes());
?>
<?php if ($_product->isSaleable() && count($_attributes)):?>
    <dl>
    <?php foreach($_attributes as $_attribute): ?>
        <dt><label class="required"><em>*</em><?php echo $_attribute->getLabel() ?></label></dt>
        <dd<?php if ($_attribute->decoratedIsLast){?> class="last"<?php }?>>
            <div class="input-box">
                <select name="super_attribute[<?php echo $_attribute->getAttributeId() ?>]" id="attribute<?php echo $_attribute->getAttributeId() ?>" class="required-entry super-attribute-select">
                    <option><?php echo $this->__('Choose an Option...') ?></option>
                  </select>
              </div>
        </dd>
    <?php endforeach; ?>
    </dl>
    
    <?php //"Hole Punched" block/placeholder ?>
    <?php echo $this->getChildHtml('live_json_config') ?>    
    
<?php endif;?>

How to Call the jsonconfig template

Lastly we have what I called the jsonconfig template. There is only one line of code, which does exactly what it did originally, however because its in its own template now we are able to have it be “Hole Punched” and ran separately from the rest of the content when needed.

app/design/frontend/base/default/template/demac/liveconfigurables/jsonconfig.phtml

<script type="text/javascript">
    var spConfig = new Product.Config(<?php echo $this->getJsonConfig() ?>);
</script>

I hope you found this useful, to make it even easier for your to test and play around with I have a attached a copy of the code here: Demac_LiveConfigurables.zip