Categories


Archives


Recent Posts


Categories


Imagine 2014: Magento 1.9 Infinite Theme Fallback

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

This entry is part 38 of 43 in the series Miscellaneous Magento Articles. Earlier posts include Magento Front Controller, Reinstalling Magento Modules, Clearing the Magento Cache, Magento's Class Instantiation Abstraction and Autoload, Magento Development Environment, Logging Magento's Controller Dispatch, Magento Configuration Lint, Slides from Magento Developer's Paradise, Generated Magento Model Code, Magento Knowledge Base, Magento Connect Role Directories, Magento Base Directories, PHP Error Handling and Magento Developer Mode, Magento Compiler Mode, Magento: Standard OOP Still Applies, Magento: Debugging with Varien Object, Generating Google Sitemaps in Magento, IE9 fix for Magento, Magento's Many 404 Pages, Magento Quickies, Commerce Bug in Magento CE 1.6, Welcome to Magento: Pre-Innovate, Magento's Global Variable Design Patterns, Magento 2: Factory Pattern and Class Rewrites, Magento Block Lifecycle Methods, Goodnight and Goodluck, Magento Attribute Migration Generator, Fixing Magento Flat Collections with Chaos, Pulse Storm Launcher in Magento Connect, StackExchange and the Year of the Site Builder, Scaling Magento at Copious, Incremental Migration Scripts in Magento, A Better Magento 404 Page, Anatomy of the Magento PHP 5.4 Patch, Validating a Magento Connect Extension, Magento Cross Area Sessions, and Review of Grokking Magento. Later posts include Magento Ultimate Module Creator Review, Magento Imagine 2014: Parent/Child Themes, Early Magento Session Instantiation is Harmful, Using Squid for Local Hostnames on iPads, and Magento, Varnish, and Turpentine.

The new Magento parent/child infinite theme fallback system is the first time in years Magento’s added a new significant system to the framework code. As with any new system we need to reconcile our expectations vs. the actual behavior of the system. Put another way, we need to figure out how things work, what’s a bug, and what’s just intended-but-weird behavior.

This article is going to explore the root cause of some behavior discovered by Eric Wiese. The first half of the article we’ll be talking about code deep in Magento’s guts. If you don’t following along 100% don’t worry, there’s practical code samples starting at the Theme Based Layout XML Updates section.

File Fallback

Since the earliest days of Magento, the core/design_package model has handled the business logic for Magento’s theming system

#File: app/code/core/Mage/Core/Model/Design/Package.php
class Mage_Core_Model_Design_Package
{
}

The “business logic” here is how to resolve requests for a “design file” of a specific type (javascript, layout update XML, phtml template, etc) to a specific server file based on the currently configured package, theme, and any custom design settings at System -> Design.

Covering this implementation in its entirety would be a multiple article series. The the most important method, and the one we’ll look at, is getFilename

#File: app/code/core/Mage/Core/Model/Design/Package.php
public function getFilename($file, array $params)
{
    //...
}

This method is the heart of the class, and all requests for design files run through it. The new infinite theme fallback system takes advantage of this. Since there’s a central method that handles giving out file names, it’s relatively easy to inject custom logic that implements a parent/child theme system.

In previous versions of Magento, this method used the _fallback method to reconcile a specific file path.

#MAGENTO 1.8
#File: app/code/core/Mage/Core/Model/Design/Package.php
public function getFilename($file, array $params)
{
    Varien_Profiler::start(__METHOD__);
    $this->updateParamDefaults($params);
    $result = $this->_fallback($file, $params, array(
        array(),
        array('_theme' => $this->getFallbackTheme()),
        array('_theme' => self::DEFAULT_THEME),
    ));
    Varien_Profiler::stop(__METHOD__);
    return $result;
}

This _fallback method implemented the design package fallback logic (all the way to base/default) that we’re used to today. Magento 1.9 changes this

#MAGENTO 1.9
#File: app/code/core/Mage/Core/Model/Design/Package.php
public function getFilename($file, array $params)
{
    Varien_Profiler::start(__METHOD__);
    $this->updateParamDefaults($params);        
    $result = $this->_fallback(
        $file,
        $params,
        $this->_fallback->getFallbackScheme(
            $params['_area'],
            $params['_package'],
            $params['_theme']
        )
    );
    Varien_Profiler::stop(__METHOD__);
    return $result;
}

As you can see, the code in Magento 1.9 uses a _fallback object, to get a fallback scheme, and then passes this scheme to the _fallback method. This new object is instantiated in the constructor.

#File: app/code/core/Mage/Core/Model/Design/Package.php
public function __construct()
{
    if (is_null($this->_config)) {
        $this->_config = Mage::getSingleton('core/design_config');
    }
    if (is_null($this->_fallback)) {
        $this->_fallback = Mage::getSingleton('core/design_fallback', array(
            'config' => $this->_config,
        ));
    }
}

This new core/design_fallback object contains the logic that will detect a parent theme, and create a proper fallback scheme so the parent theme is referenced when looking for a file. Also of interest, the new core/design_config object is responsible for reading and parsing the theme.xml.

What’s a fallback scheme? It’s a list of package/theme names to check for a file. For example, let’s say I have a theme named pulsestorm/a that extends a theme pulsestorm/default. The pulsestorm/default theme, in turn, extends the rwd/default theme. In this case, my fallback scheme would look like this

array (size=1)
  'frontend/pulsestorm/a' => 
    array (size=3)
      0 => 
        array (size=0)
          empty
      1 => 
        array (size=2)
          '_package' => string 'pulsestorm' (length=10)
          '_theme' => string 'default' (length=7)
      2 => 
        array (size=2)
          '_package' => string 'rwd' (length=3)
          '_theme' => string 'default' (length=7)

The first item, an empty array, signals to 1.9’s _fallback method that it should look for the file in the current design package/theme (pulsestorm/a), the second array tells the system to look in pulsestorm/default, and the last tells the system to look in rwd/default.

The specifics are worth investigating on your own, but the broad takeaway here is the core/design_fallback object looks at the theme.xml file (via the core/design_config object), and comes up with a list of themes to look for design related files in. The current Magento team leveraged the original Magento teams’s “over engineering” to drop in a theme inheritance system. This ensures every design file is subject to infinite theme inheritance.

So given this seemingly bulletproof approach, what’s this about broken infinite inheritance.

Theme Specific Layout Files

Eric’s a developer at Classy Llama, the company (along with Falkowski and — others? Let me know in the comments) responsible for the new responsive web design theme. While the Llamas didn’t build the theme fallback system, they were some of the first developers to use it. Eric’s article requires you understand all the features of theme.xml, so let’s start there.

In a previous article, we told you how to use theme.xml to specify a parent theme.

<!-- File: app/design/frontend/pulsestorm/default/etc/theme.xml -->
<?xml version="1.0"?>
<theme>
    <parent>rwd/default</parent>
</theme>

That’s not the only feature of theme.xml. In addition to specifying a parent theme you can also use theme.xml to add new layout update XML files to your theme.

<?xml version="1.0"?>
<theme>
    <parent>rwd/enterprise</parent>
    <layout>
        <updates>
            <my_new_file_name>
                <file>my_new_file_name.xml</file>
            </my_new_file_name>
        </updates>
    </layout>
</theme>

The structure is similar to adding a layout update XML file via a module’s config.xml file. Your new files go under the layout/updates node. The child node <my_new_file_name/> needs to be unique but is not used for anything semantically, and the file name itself (my_new_file_name.xml) refers to files in your theme’s layout/ folder.

This is a huge win for theme designers. Prior to this the only way to add layout XML to the system was via local.xml, or by adding a new code module. The local.xml file quickly gets clogged up with customizations, and these updates are not easily distributable. Putting your updates into a module can alleviate these problems, but with the the added complexity of having a module and having to deal with the complications arising out of when your updates run vs. updates from other modules.

Having layout update XML files in theme.xml solves all this. Theme developers can specify which files their themes use in the theme itself, and Magento processes theme.xml after module configured layout update XML files but before local.xml.

There is, however, one catch to be aware of. This brings us to Eric’s article.

No Inheritance in Themes

The short version: Layout update files in theme.xml are ignored by Magento’s new inheritance system. When Magento examines the configuration for layout update xml files, it does so here.

#File: app/code/core/Mage/Core/Model/Layout/Update.php
public function getFileLayoutUpdatesXml($area, $package, $theme, $storeId = null)
{
    //...

    //grabs files from `config.xml`
    $updates = $updatesRoot->asArray();

    //grabs files from `theme.xml`
    $themeUpdates = Mage::getSingleton('core/design_config')->getNode("$area/$package/$theme/layout/updates");

    if ($themeUpdates && is_array($themeUpdates->asArray())) {
        //array_values() to ensure that theme-specific layouts don't override, but add to module layouts
        $updates = array_merge($updates, array_values($themeUpdates->asArray()));
    }     

    //...

    //local.xml still wins in the end
    $updateFiles[] = 'local.xml';
}

As you can see, Magento directly queries the core/design_config object for the new layout update XML file names — it does nothing to resolve the theme inheritance chain. This means if you have a theme hierarchy like this

'pulsestorm/a` extends 
    `pulsestorm/default` 
        extends 'rwd/default`

then layout update XML configured in pulsestorm/default will be ignored when the theme is set to pulsestorm/a. As we learned earlier, the inheritance system works because every time you request a file, Magento references a fallback scheme to determine where the file should be loaded from. Since the layout update loading code never looks at information in other themes, it never sees the other files, and never has the chance to ask the fallback system where these files are located.

We’ve been talking theoretical concepts for a while now. Next we’re going to get concrete with some code that should make this a lot clearer. If you’re running into trouble with the examples below, the final theme code is located in this GitHub repository.

Theme Based Layout XML Updates

To start with, we’ll create a pulsestorm/default theme by creating the following folder structure/file

#File: app/design/frontend/pulsestorm/default/etc/theme.xml
<?xml version="1.0"?>
<theme>
    <parent>rwd/default</parent>
</theme>

Our new theme is a child of the rwd/default theme. If we enable our theme in Magento’s backend at System -> Configuration -> Design

and clear our Magento cache (to pickup changes to theme.xml) we should see the responsive theme when we load our homepage.

Next, we’re going to add a layout update XML file that adds a custom message to our CMS homepage’s content area.

#File: app/design/frontend/pulsestorm/default/layout/hello.xml
<layouts>
    <cms_index_index>
        <reference name="content">
            <block type="core/text" name="pulsestorm_hello_message" alias="pulsestorm_hello_message">
                <action method="setText">
                    <text><![CDATA[
                    <h2>Hello World</h2>
                    ]]></text>
                </action>
            </block>
        </reference>
    </cms_index_index>
</layouts>

This is a pretty standard layout update XML file. We’re hooking into the cms_index_index handle (i.e. when the home page loads), getting a reference to the content block, and adding a new text block to it. What’s different is our next step. We’re going to add a layout/update node to our theme.xml file instead of a new module’s config.xml file.

#File: app/design/frontend/pulsestorm/default/etc/theme.xml
<?xml version="1.0"?>
<theme>
    <parent>rwd/default</parent>
    <layout>
        <updates>
            <some_unique_identifier_pulsestorm>
                <file>hello.xml</file>
            </some_unique_identifier_pulsestorm>
        </updates>
    </layout>    
</theme>

Here we’ve told our theme to include the layout file named hello.xml. If we clear our cache and reload the homepage, we’ll see the following

Congratulations — you just added your first theme specific layout XML update file. Let’s add a second one for a Goodbye message. This time we’ll add our theme.xml configuration first

#File: app/design/frontend/pulsestorm/etc/theme.xml
<?xml version="1.0"?>
<theme>
    <!-- ... -->
    <layout>
        <updates>
            <!-- ... -->
            <another_unique_identifier_pulsestorm>
                <file>goodbye.xml</file>
            </another_unique_identifier_pulsestorm>
        </updates>
    </layout>    
</theme>

and then add a new goodbye.xml file to our layout folder.

#File: app/design/frontend/pulsestorm/default/layout/goodbye.xml
<layouts>
    <cms_index_index>
        <reference name="content">
            <block type="core/text" name="pulsestorm_goodbye_message" alias="pulsestorm_goodbye_message">
                <action method="setText">
                    <text><![CDATA[
                    <h2>Goodbye World</h2>
                    ]]></text>
                </action>
            </block>
        </reference>
    </cms_index_index>
</layouts>

Clear your cache, reload your home page, and you should now see both messages.

While this is a simple exploration of the feature, I’m sure you can see how having your theme specify the layout updates is a useful and powerful thing.

Finally, for contrast, we’re going to show how theme inheritance also works with module based layout files. Let’s copy cms.xml from the base design package

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

into our theme at

app/design/frontend/pulsestorm/default/layout/cms.xml

and then comment the following nodes

<!-- #File: app/design/frontend/pulsestorm/default/layout/cms.xml -->
<!-- ... -->
<cms_page translate="label">
    <label>CMS Pages (All)</label>
<!-- 
    <reference name="content">
        <block type="core/template" name="page_content_heading" template="cms/content_heading.phtml"/>
        <block type="page/html_wrapper" name="cms.wrapper" translate="label">
            <label>CMS Content Wrapper</label>
            <action method="setElementClass"><value>std</value></action>
            <block type="cms/page" name="cms_page"/>
        </block>
    </reference>
 -->
</cms_page>
<!-- ... -->

If you clear your cache reload the page with the above in place, you’ll see we’ve lost our Home Page heading.

Again, an oversimplified example that you’d never use in the real world, but it demonstrates how theme inheritance works.

Infinite Fallback?

Next, we’re going to create another theme that uses pulsestorm/default as its parent. We’ll call this new theme pulsestorm/a

#File: app/design/frontend/pulsestorm/a/etc/theme.xml
<?xml version="1.0"?>
<theme>
    <parent>pulsestorm/default</parent>
</theme>    

To enable this theme all we’ll need to do is set the design package to pulsestorm and the theme name to a.

Now, if we clear our cache and reload the homepage, we’ll see our theme correctly displays the responsive design theme (picking up on the fact that pulsestorm/a‘s parent (pulsestorm/default) is a child of rwd/default).

However, there’s something odd going on with the Layout XML files. We’ve correctly inherited the cms.xml file from pulsestorm/default — we can tell because there’s no Home Page title. However, we have not inherited either hello.xml or goodbye.xml.

This is the problem Eric describes in his article — not that infinite fallback ignores all layout xml files, but that it ignores layout xml files configured via theme.xml.

Reasons and Solutions

Eric describes this as a failure of the infinite inheritance system — while he doesn’t use the word it’s clear he sees it as a bug, and like any good developer he created a great module to make the system behave more to his expectations.

When I look at this behavior, I’m not sure if it’s a bug, a deliberate design decision, or an unthought through consequence of a deliberate design decision.

If Magento was the product of one mind I’d say it was a deliberate choice to have the inheritance system ignore theme.xml layout files. If you want a layout file available to all parent and child themes, use a module. If you want it only available to your specific theme, use theme.xml. In this version of the world theme.xml becomes a way to organize updates that normally go into local.xml.

However, Magento’s not the product of one mind. It’s the product of executives directing product managers directing project managers/producers who direct the developers who implement features with the community throwing its weight behind certain things. Maybe this one line

$themeUpdates = Mage::getSingleton('core/design_config')->getNode("$area/$package/$theme/layout/updates");

was a deliberate decision to ignore other themes. Or maybe the engineers came up with the fallback solution to the theme problem, and didn’t think through how the layout update loading would work. Or maybe they did think it through and the new implementation details were too nuanced to complete in their given schedule so they went with the simpler solution.

Regardless, if this is a bug I’d expect to see it addressed in a future point release. In the meantime, if you want your 2nd and 3rd level child themes to use their parent’s layout update XML files, you have two choices. The first? Install Eric’s module and themes will inherit their parent’s theme.xml layout update files.

The other solution? Just configure your child’s theme.xml layout file to include the other layout update XML files. For example, if we wanted pulsestorm/a to have the hello.xml file, we’d create a theme.xml that looks like this

#File: app/design/frontend/pulsestorm/a/etc/theme.xml
<?xml version="1.0"?>
<theme>
    <parent>pulsestorm/default</parent>
    <layout>
        <updates>
            <yet_another_unique_node_name>
                <file>hello.xml</file>
            </yet_another_unique_node_name>
        </updates>
    </layout>
</theme>

Clear you cache, reload the homepage, and you’ll see your Hello World message

Notice we didn’t move hello.xml into our theme, all we did was tell our theme about the file via theme.xml, and the fallback system found it in pulsestorm/default. So, from this point of view, it’s not that the fallback system fails to inherit files from theme.xml — it’s that it gives you full control over which layout update XML files get pulled in.

Wrap Up

I’ll be the first to admit this may be spinning a bug as a feature. At the same time I’ve met enough engineering folks to know this may be the intent of the system. The larger lesson here is modern software systems are complex — more complex than any one human can understand, and as humans we’re going to bring our own points of view to table. Being able to clearly and honestly communicate during a software project is hugely important, both in implementing the correct features and understanding intended system behavior.

If you can nurture the talent of adopting multiple points of view throughout your career, you’ll find not only will it help you deal with the people you meet along the way — but it will also help you better understand the systems you’ll be working with and building.

Originally published May 20, 2014
Series Navigation<< Review of Grokking MagentoMagento Ultimate Module Creator Review >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 20th May 2014