Eric Wiese

Magento Infinite Theme Fallback Fix

Overview

Magento EE 1.14 and CE 1.9 add the much-needed capability for infinite theme fallback. This allows themes to be broken into layers of abstractions, reducing template duplication and allowing more specific themes to be easily customized while still inheriting upstream updates from more generic themes.

In addition, these Magento versions add the ability for themes to cleanly indicate theme-specific layout XML update files via etc/theme.xml. This is extremely powerful, as it permits efficient and light-handed layout updates to be properly abstracted and organized much better than the old local.xml.

By using both these features, complex sites which employ many stores views for targeted sites or internationalization can greatly reduce the amount of work and code maintenance that is required for highly targeted themes.

However, the native Magento theme fallback loading of layout XML files has a severe shortcoming.

Example scenario

Let’s pretend I have a client which requires a base theme which will be used by all Magento stores. Country- or product-specific store views can create custom themes which fall back through the base theme, ensuring they always are up to date with the latest improvements to the base theme but allowing them to customize and override the base theme where necessary.

In particular, due to corporate standards, every site must have a corporate copyright notice on all pages. No problem – we’ll just drop a core/text block into a theme-specific layout XML update file and set the text there! (That’s clearly the best way and not at all contrived … right? right?)

In no time flat, I’ve carved out a new package for the company and corporate-base theme, obviously employing the new theme.xml ability to add theme-specific layout update XML files to accomplish the objective. Let’s take a look at the new theme:

mycompany/corporate-base theme dir layout
Theme dir layout
<?xml version="1.0"?>
<theme>
    <parent>rwd/enterprise</parent>
    <layout>
        <updates>
            <corporate_base_default>
                <file>corporate-base/default.xml</file>
            </corporate_base_default>
        </updates>
    </layout>
</theme>

mycompany/corporate-base/etc/theme.xml

<?xml version="1.0"?>
<layout>

    <default>
        <reference name="content">
            <block type="core/text" name="corporatebase.copyright" before="-">
                <action method="setText">
                    <text>
                        <![CDATA[
                                <h1>&copy; Corporate Base</h1>
                        ]]>
                    </text>
                </action>
            </block>
        </reference>
    </default>

</layout>

mycompany/corporate-base/layout/corporate-base/default.xml

After a quick layout cache flush and refresh the corporate base copyright is plain for all to see.

Homepage screenshot after corporate base layout update applied screenshot

Alright, halfway there …

US-specific theme copyright

For very important legal reasons, the US site must display its own copyright notice, in addition to the corporate base notice. No big deal – it should be as simple as a us-specific theme which falls back through the corporate-base theme and adds its own layout update file in a similar manner.

Et voila …

mycompany package with both themes layout screenshot
Updated theme dir layout
<?xml version="1.0"?>
<theme>
    <parent>mycompany/corporate-base</parent>
    <layout>
        <updates>
            <us_default>
                <file>us/default.xml</file>
            </us_default>
        </updates>
    </layout>
</theme>

mycompany/us/etc/theme.xml

<?xml version="1.0"?>
<layout>

    <default>
        <reference name="content">
            <block type="core/text" name="us.copyright" after="corporatebase.copyright">
                <action method="setText">
                    <text>
                        <![CDATA[
                                <h1>&copy; US theme</h1>
                        ]]>
                    </text>
                </action>
            </block>
        </reference>
    </default>

</layout>

mycompany/us/layout/us/default.xml

Infinite, custom theme fallback makes things so much easier … this is so slick – I should just go ahead and push straight to production!

Houston, we have a problem

Actually, I guess I’d better test – just for the fun of it.

Homepage screenshot after US theme layout update applied screenshot

Hmmm … the US copyright showed up as expected, but the corporate base copyright disappeared. It appears the US theme’s layout update file is being loaded, but the corporate-base layout update file isn’t.

After some debugging, it becomes apparent that there is an oversight.

Mage_Core_Model_Layout_Update::getFileLayoutUpdatesXml() annotated screenshot
Mage_Core_Model_Layout_Update::getFileLayoutUpdatesXml()

Wait … it never loads layout update files for the current theme’s parent(s)! The corporate-base layout update file is indeed not being loaded.

The solution

There are two problems to be solved to fix this issue.

  1. Determine the current theme’s parent theme(s)
  2. Inject the parent theme(s) theme.xml layout update files

Determine the current theme’s parent theme(s)

More debugging leads me to the promising sounding method Mage_Core_Model_Design_Fallback::getFallbackScheme().

After a quick test, this method returns the parent theme(s) for the area, package, and theme passed in as parameters.

var_dump(Mage::getModel('core/design_fallback')->getFallbackScheme('frontend','mycompany','us'));

results in

Mage_Core_Model_Design_Fallback::getFallbackScheme() return value screenshot

Inject the parent theme(s) theme.xml layout update files

This could have required a nasty model rewrite, but some oh-so-clever developer at Magento left us a sparkling event at just the right time.

The core_layout_update_updates_get_after event passes in the updates as a Mage_Core_Model_Config_Element just after module updates are loaded but just before the current theme’s theme.xml layout updates. This is a perfect opportunity to inject the parent theme(s) layout update files.

Final module

After gather the information required to fix the issue, a new module is born. Observing the core_layout_update_updates_get_after event, each parent theme(s) theme.xml layout updates are loaded and injected into the final update object. When the observer returns control, the Mage_Core_Model_Layout_Update::getFileLayoutUpdatesXml() method will blissfully add the final updates (the current theme’s theme.xml layout updates and local.xml) – everything is as it should be.

app/code/community/EW/ThemeFallbackFix/Model/Observer.php

<?php

class EW_ThemeFallbackFix_Model_Observer extends Mage_Core_Model_Abstract
{
    /**
     * Add layout files added via theme.xml to layout updates
     * for all themes that are parents of this theme.
     * Observes: core_layout_update_updates_get_after
     *
     * @param Varien_Event_Observer $observer
     */
    public function addFallbackThemesLayoutUpdates(Varien_Event_Observer $observer) {
        /* @var $updates Mage_Core_Model_Config_Element */
        $updates = $observer->getUpdates();
        /* @var $designPackage Mage_Core_Model_Design_Package */
        $designPackage = Mage::getSingleton('core/design_package');
        /* @var $fallback Mage_Core_Model_Design_Fallback */
        $fallback = Mage::getModel('core/design_fallback');

        $fallbacks = $fallback->getFallbackScheme($designPackage->getArea(), $designPackage->getPackageName(), $designPackage->getTheme('layout'));

        for($i=count($fallbacks)-1; $i>=0; $i--) {
            $fallback = $fallbacks[$i];
            if(!isset($fallback['_package']) || !isset($fallback['_theme'])) {
                continue;
            }

            $fallbackPackage = $fallback['_package'];
            $fallbackTheme = $fallback['_theme'];

            $themeUpdateGroups = Mage::getSingleton('core/design_config')->getNode("{$designPackage->getArea()}/$fallbackPackage/$fallbackTheme/layout/updates");

            if(!$themeUpdateGroups) {
                continue;
            }

            foreach($themeUpdateGroups as $themeUpdateGroup) {
                $themeUpdateGroupArray = $themeUpdateGroup->asArray();

                foreach($themeUpdateGroupArray as $key => $themeUpdate) {
                    $updateNode = $updates->addChild($key);
                    $updateNode->addChild('file', $themeUpdate['file']);
                }
            }
        }
    }
}

Results

After adding the fix module and the cache is flushed, it’s time to test again.

Final screenshot after installing module

Alright! I think I deserve a coffee break …

Where to get it

I know what you’re thinking – “I have clients that could benefit from multiple levels of theme fallback”.

Simply head over to the magento theme fallback fix github page and install – It’s even modman-ready. Use it wisely!

Discussion