Magento Theme Fallback

Magento has unique functionality that will allow you to configure a package and theme for your store. Magento checks these values configured when firstly running the preDispatch() method.

public function preDispatch()
{
    if (!$this->getFlag('', self::FLAG_NO_CHECK_INSTALLATION)) {
        if (!Mage::isInstalled()) {
            $this->setFlag('', self::FLAG_NO_DISPATCH, true);
            $this->_redirect('install');
            return;
        }
    }

    // Prohibit disabled store actions
    if (Mage::isInstalled() && !Mage::app()->getStore()->getIsActive()) {
        Mage::app()->throwStoreException();
    }

    if ($this->_rewrite()) {
        return;
    }

    if (!$this->getFlag('', self::FLAG_NO_START_SESSION)) {
        $checkCookie = in_array($this->getRequest()->getActionName(), $this->_cookieCheckActions)
            && !$this->getRequest()->getParam('nocookie', false);
        $cookies = Mage::getSingleton('core/cookie')->get();
        /** @var $session Mage_Core_Model_Session */
        $session = Mage::getSingleton('core/session', array('name' => $this->_sessionNamespace))->start();

        if (empty($cookies)) {
            if ($session->getCookieShouldBeReceived()) {
                $this->setFlag('', self::FLAG_NO_COOKIES_REDIRECT, true);
                $session->unsCookieShouldBeReceived();
                $session->setSkipSessionIdFlag(true);
            } elseif ($checkCookie) {
                if (isset($_GET[$session->getSessionIdQueryParam()]) && Mage::app()->getUseSessionInUrl()
                    && $this->_sessionNamespace != Mage_Adminhtml_Controller_Action::SESSION_NAMESPACE
                ) {
                    $session->setCookieShouldBeReceived(true);
                } else {
                    $this->setFlag('', self::FLAG_NO_COOKIES_REDIRECT, true);
                }
            }
        }
    }

    Mage::app()->loadArea($this->getLayout()->getArea());
    ....
}

The key line to look at here is:

Mage::app()->loadArea($this->getLayout()->getArea());

The loadArea() method can be seen below.

public function loadArea($code)
{
    $this->getArea($code)->load();
    return $this;
}

The load() method will, if no argument has been specified, load the config, events, design and translate parts of the system via the _loadPart() method.

public function load($part=null)
{
    if (is_null($part)) {
        $this->_loadPart(self::PART_CONFIG)
            ->_loadPart(self::PART_EVENTS)
            ->_loadPart(self::PART_DESIGN)
            ->_loadPart(self::PART_TRANSLATE);
    }
    else {
        $this->_loadPart($part);
    }
    return $this;
}
protected function _loadPart($part)
{
    if (isset($this->_loadedParts[$part])) {
        return $this;
    }
    Varien_Profiler::start('mage::dispatch::controller::action::predispatch::load_area::'.$this->_code.'::'.$part);
    switch ($part) {
        case self::PART_CONFIG:
            $this->_initConfig();
            break;
        case self::PART_EVENTS:
            $this->_initEvents();
            break;
        case self::PART_TRANSLATE:
            $this->_initTranslate();
            break;
        case self::PART_DESIGN:
            $this->_initDesign();
            break;
    }
    $this->_loadedParts[$part] = true;
    Varien_Profiler::stop('mage::dispatch::controller::action::predispatch::load_area::'.$this->_code.'::'.$part);
    return $this;
}

The _initDesign() method instantiates a singleton of the Mage_Core_Model_Design_Package class.

protected function _initDesign()
{
    if (Mage::app()->getRequest()->isStraight()) {
        return $this;
    }
    $designPackage = Mage::getSingleton('core/design_package');
    if ($designPackage->getArea() != self::AREA_FRONTEND)
        return;

    $currentStore = Mage::app()->getStore()->getStoreId();

    $designChange = Mage::getSingleton('core/design')
        ->loadChange($currentStore);

    if ($designChange->getData()) {
        $designPackage->setPackageName($designChange->getPackage())
            ->setTheme($designChange->getTheme());
    }
}

The __construct() magic method of the Mage_Core_Model_Design config checks to see if there is a theme.xml file present in a theme’s etc directory.

If there is, Magento loads the contents of the file into a new config object. This will come in use for later when Magento checks the fallback scheme.

public function __construct(array $params = array())
{
    if (isset($params['designRoot'])) {
        if (!is_dir($params['designRoot'])) {
            throw new Mage_Core_Exception("Design root '{$params['designRoot']}' isn't a directory.");
        }
        $this->_designRoot = $params['designRoot'];
    } else {
        $this->_designRoot = Mage::getBaseDir('design');
    }
    $this->_cacheChecksum = null;
    $this->setCacheId('config_theme');
    $this->setCache(Mage::app()->getCache());
    if (!$this->loadCache()) {
        $this->loadString('<theme />');
        $path = str_replace('/', DS, $this->_designRoot . '/*/*/*/etc/theme.xml');
        $files = glob($path);
        foreach ($files as $file) {
            $config = new Varien_Simplexml_Config();
            $config->loadFile($file);
            list($area, $package, $theme) = $this->_getThemePathSegments($file);
            $this->setNode($area . '/' . $package . '/' . $theme, null);
            $this->getNode($area . '/' . $package . '/' . $theme)->extend($config->getNode());
        }
        $this->saveCache();
    }
}

When Magento gets the list of layout and template files to render, it runs through the getFileName() method.

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;
}

Here we are storing the result of the _fallBack() method into a $result variable. _fallBack() takes three arguments.

  • The file name
  • Any additional parameters
  • The fallback scheme via the getFallbackScheme() method.

The getFallbackScheme() method is as follows.

public function getFallbackScheme($area, $package, $theme)
{
    $cacheKey = $area . '/' . $package . '/' . $theme;

    if (!isset($this->_cachedSchemes[$cacheKey])) {

        if ($this->_isInheritanceDefined($area, $package, $theme)) {
            $scheme = $this->_getFallbackScheme($area, $package, $theme);
        } else {
            $scheme = $this->_getLegacyFallbackScheme();
        }

        $this->_cachedSchemes[$cacheKey] = $scheme;
    }

    return $this->_cachedSchemes[$cacheKey];
}

So there is an if statement using the _isInheritanceDefined() method. If it returns true, we return a $scheme using the _getFallbackScheme() method. If it returns false, we return a $scheme of _getLegacyFallbackScheme().

The difference between the two schemes is that the legacy fallback scheme was the scheme prior to Magento Comunity/Open Source version 1.9 and Enterprise/Commerce version 1.13 whereby the fallback scheme would follow the convention below.

  • package/theme
  • package/default
  • base/default

We can see this in the _isInheritanceDefined() method.

protected function _isInheritanceDefined($area, $package, $theme)
{
    $path = $area . '/' . $package . '/' . $theme . '/parent';
    return $this->_config->getNode($path) !== false;
}

So it’s here where the nodes are checked within the theme.xml file.

protected function _getFallbackScheme($area, $package, $theme)
{
    $scheme = array(array());
    $this->_visited = array();
    while ($parent = (string)$this->_config->getNode($area . '/' . $package . '/' . $theme . '/parent')) {

        $this->_checkVisited($area, $package, $theme);

        $parts = explode('/', $parent);
        if (count($parts) !== 2) {
            throw new Mage_Core_Exception('Parent node should be defined as "package/theme"');
        }
        list($package, $theme) = $parts;
        $scheme[] = array('_package' => $package, '_theme' => $theme);
    }

    return $scheme;
}
protected function _getLegacyFallbackScheme()
{
    return array(
        array(),
        array('_theme' => $this->_getFallbackTheme()),
        array('_theme' => Mage_Core_Model_Design_Package::DEFAULT_THEME),

The _getFallbackTheme() method will fetch the value saved in the admin area under System -> Configuration -> Design, under the Theme section in the default field.

protected function _getFallbackTheme()
{
    return $this->getStore()->getConfig('design/theme/default');
}

And the _fallBack() method validates the file and sets the package and theme to base and default respectively.

protected function _fallback($file, array &$params, array $fallbackScheme = array(array()))
{

    if ($this->_shouldFallback) {
        foreach ($fallbackScheme as $try) {
            $params = array_merge($params, $try);
            $filename = $this->validateFile($file, $params);
            if ($filename) {
                return $filename;
            }
        }
        $params['_package'] = self::BASE_PACKAGE;
        $params['_theme']   = self::DEFAULT_THEME;
            
    }
    return $this->_renderFilename($file, $params);
}

Note: This article is based on Magento Community/Open Source version 1.9.