Categories


Archives


Recent Posts


Categories


The Magento Global Config, Revisited

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.

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

If you’ve spent any time with Magento, you know the importance the “global config” plays in a the system. However, you may not fully understand how that configuration tree is built. Usually, most tutorial authors (myself included), will gloss over this and say something like

All the module’s etc/config.xml files are merged into one

While this statement is true, it’s only part of the story. Understanding what’s in this tree, and how it’s loaded, is a necessary step on the road to Magento mastery. There’s more than just module configuration in the Magento global config. Over the next few articles we’ll cover exactly what’s in the configuration tree, and where it comes from.

The specifics of this article refer to Magento 1.6.1 CE, but the principles discussed apply to all versions.

Object Hierarchy

If you open up the main Mage class file, you’ll see the run method

#File: app/Mage.php
public static function run($code = '', $type = 'store', $options = array())
{        
    try {
        Varien_Profiler::start('mage');
        self::setRoot();
        self::$_app    = new Mage_Core_Model_App();
        self::$_events = new Varien_Event_Collection();
        self::$_config = new Mage_Core_Model_Config($options);
        self::$_app->run(array(
            'scope_code' => $code,
            'scope_type' => $type,
            'options'    => $options,
        ));
        Varien_Profiler::stop('mage');
    }
    ...

Magento calls this static method at the bottom of the index.php bootstrap file. The code in the run method is responsible for executing a full Magento HTTP request. If you remove the profiling lines, that means Magento is only 5 lines of code! Who says it’s complicated?

The line we’re interested in is

#File: app/Mage.php
self::$_config = new Mage_Core_Model_Config($options);

This is where the Magento global config is instantiated, and this is the object that will be used to load and interact with Magento’s XML configuration files. For those new to Magento, the Layout XML files are a separate system, covered in my No Frills Magento Layout book. Layout XML files are not strictly configuration files. Instead, Layout XML files form the basis for a domain specific programming language which allows users to interact with the Layout and Block objects. Because of this, they don’t fall under the domain of Mage_Core_Model_Config, and will not be covered in this series.

The Mage_Core_Model_Config class extends the Mage_Core_Model_Config_Base class

#File: app/code/core/Mage/Core/Model/Config.php
class Mage_Core_Model_Config extends Mage_Core_Model_Config_Base
{
}

which in turn extends a Varien_Simplexml_Config class

#File: app/code/core/Mage/Core/Model/Config/Base.php
class Mage_Core_Model_Config_Base extends Varien_Simplexml_Config
{
}

which is a general class for dealing with XML configuration files. These classes exist to create a structure and “configuration context” around PHP’s generic XML objects. By ensuring Magento client developers always ask for configuration information via the Config object, Magento system developers can provide those client developers with shared functionality, and also allow them to take advantage of functionality like caching without having to change their code.

Note: Client here doesn’t refer to “client side browser development”. Instead, client developer means someone using Magento to create a store or application. These people are clients of the of the Magento system. The line between client and service developer is often blurred in Magento, but it’s still a useful metaphor for understanding the intent of core code.

Loading the Config

That’s the config object instantiated, but so far we haven’t loaded any XML files. What gives? If you take a look at the class’s constructor

#File: app/code/core/Mage/Core/Model/Config.php
public function __construct($sourceData=null)
{
    $this->setCacheId('config_global');
    $this->_options         = new Mage_Core_Model_Config_Options($sourceData);
    $this->_prototype       = new Mage_Core_Model_Config_Base();
    $this->_cacheChecksum   = null;
    parent::__construct($sourceData);
}

we see some code, but at first glance the code doesn’t look like it loads any XML. That glance would be correct, but we’ll come back to this constructor code it a bit, so keep it in the back of your mind.

To find the point in the application startup process where an XML tree is loaded, we need to follow the application startup execution chain a few methods down the stack. We’ll be breezing past some things that are worth exploring, but don’t relate directly to configuration loading.

We’ll start with the app model object’s run method, which is called by the Mage class’s run method

#File: app/code/core/Mage/Core/Model/App.php
public function run($params)
{
    $options = isset($params['options']) ? $params['options'] : array();
    $this->baseInit($options);
    Mage::register('application_params', $params);        
...

Early on in this method, a call to $this->baseInit($options); is made. Looking at this method

#File: app/code/core/Mage/Core/Model/App.php
public function baseInit($options)
{
    $this->_initEnvironment();

    $this->_config = Mage::getConfig();
    $this->_config->setOptions($options);

    $this->_initBaseConfig();
    $cacheInitOptions = is_array($options) && array_key_exists('cache', $options) ? $options['cache'] : array();
    $this->_initCache($cacheInitOptions);

    return $this;
}

we can se two items of interest. First, we’re assigning a reference to the previously defined config object the the app model object’s _config property

#File: app/code/core/Mage/Core/Model/App.php
$this->_config = Mage::getConfig();
$this->_config->setOptions($options);

This gives the app model object easy access to the config. Remember, in PHP 5+ objects are references. This means both the Mage class and the core/app object have a reference to the same config object. When one changes, they both change.

Out second item of interest is the call to the _initBaseConfig

#File: app/code/core/Mage/Core/Model/App.php
protected function _initBaseConfig()
{
    Varien_Profiler::start('mage::app::init::system_config');
    $this->_config->loadBase();
    Varien_Profiler::stop('mage::app::init::system_config');
    return $this;
}

The _initBaseConfig method makes a call to the configuration object’s loadBase method. This method is where the first XML files will be loaded from disk. Let’s take a look!

#File: app/code/core/Mage/Core/Model/Config.php
public function loadBase()
{
    $etcDir = $this->getOptions()->getEtcDir();
    $files = glob($etcDir.DS.'*.xml');

    $this->loadFile(current($files));
    while ($file = next($files)) {
        $merge = clone $this->_prototype;
        $merge->loadFile($file);
        $this->extend($merge);
    }
    if (in_array($etcDir.DS.'local.xml', $files)) {
        $this->_isLocalConfigLoaded = true;
    }
    return $this;
}

The first two lines of this method

#File: app/code/core/Mage/Core/Model/Config.php
$etcDir = $this->getOptions()->getEtcDir();
$files = glob($etcDir.DS.'*.xml');

will “glob up” any XML files in the app/etc folder. In a standard base system, the value returned by a call to $this->getOptions()->getEtcDir(); is /path/to/magento/app/etc. In a normally operating system the $files variable will contain the following two strings.

/path/to/magento/app/etc/local.xml
/path/to/magento/app/etc/config.xml

Next, we see the following

#File: app/code/core/Mage/Core/Model/Config.php
$this->loadFile(current($files));

This code takes the first filename in that array, and passes it to the loadFile method. Unless you’ve been using PHP for a very long time, you may not be familiar with the current method. Time was, PHP didn’t have a handy foreach loop. In versions prior to PHP 4, the only way to loop over an array was to use a for($i=0;$i<count($array);$i++) style construct or use PHP’s internal array functions (current, next, etc. to manipulate an arrays internal “pointer”.

Why this construct is being used here is unclear. Perhaps there’s a performance gain, or maybe the core developer was new enough to PHP that they didn’t know about the newer constructs and current was encountered before array_shift. Regardless, all you need to know is the first item of the array, (/path/to/magento/app/etc/config.xml), is being passed to the loadFile method. This method is located on the base Varien_Simplexml_Config class.

#File: lib/Varien/Simplexml/Config.php
public function loadFile($filePath)
{
    if (!is_readable($filePath)) {
        //throw new Exception('Can not read xml file '.$filePath);
        return false;
    }

    $fileData = file_get_contents($filePath);
    $fileData = $this->processFileData($fileData);
    return $this->loadString($fileData, $this->_elementClass);
}

Here we have another small, compact method. First, if the file isn’t readable by PHP (root owned, improper permissions, etc.) Magento will bail on attempting to load it.

#File: lib/Varien/Simplexml/Config.php
if (!is_readable($filePath)) {
    //throw new Exception('Can not read xml file '.$filePath);
    return false;
}

Next, the contents of the file are loaded into memory and passed through the processFileData method.

#File: lib/Varien/Simplexml/Config.php
$fileData = file_get_contents($filePath);
$fileData = $this->processFileData($fileData);

The processFileData method implements a listener method for classes which sub-class Varien_Simplexml_Config. As you can see, the base implementation does nothing

#File: lib/Varien/Simplexml/Config.php
public function processFileData($text)
{
    return $text;
}

The classes which inherit from Varien_Simplexml_Config may implement their own processFileData method to manipulate the XML string before loading it. The Mage_Core_Model_Config class doesn’t take advantage of this for anything, but it’s worth noting, as you never know what another developer may have done with a class rewrite.

Finally, we pass the data to the loadString method

#File: lib/Varien/Simplexml/Config.php
return $this->loadString($fileData, $this->_elementClass);

along with the value of $this->_elementClass.

The XML Object

If we take a look at the loadString method

#File: lib/Varien/Simplexml/Config.php
public function loadString($string)
{
    if (is_string($string)) {
        $xml = simplexml_load_string($string, $this->_elementClass);
        if ($xml instanceof Varien_Simplexml_Element) {
            $this->_xml = $xml;
            return true;
        }
    } else {
        Mage::logException(new Exception('"$string" parameter for simplexml_load_string is not a string'));
    }
    return false;
}

we can see that loadString is responsible for creating a SimpleXML object from the passed in string, and assigning that object to the internal $this->_xml property after ensuring its an instance of Varien_Simplexml_Element.

Wait, what? Where does Varien_Simplexml_Element come from? Shouldn’t a simple XML object be an instance of SimpleXMLElement? Turns out there’s a little known/used feature of the simple xml loading functions that allows you to specify a custom class for your SimpleXMLElement objects. This allows you to create custom methods that are available directly on any simple XML objets you create. That’s because the objects are no longer SimpleXMLElements, they’re now objects that are instances of your class.

If you take a look at the definition of Varien_Simplexml_Element, you can see it inherits from SimpleXMLElement, and adds multiple convenience methods for dealing with XML objects.

#File: lib/Varien/Simplexml/Element.php 
class Varien_Simplexml_Element extends SimpleXMLElement
{    
    public function setParent($element)
    {//...}

    public function getParent()
    {//...}


    public function hasChildren()
    {//...}

    public function getAttribute($name){//...}

    public function descend($path)
    {//...}

    public function asArray()
    {//...}

    public function asCanonicalArray()
    {//...}

    protected function _asArray($isCanonical = false)
    {//...}

    public function asNiceXml($filename='', $level=0)
    {//...}

    public function innerXml($level=0)
    {//...}


    public function xmlentities($value = null)
    {//...}

    public function appendChild($source)
    {//...}

    public function extend($source, $overwrite=false)
    {//...}

    public function extendChild($source, $overwrite=false)
    {//...}

    public function setNode($path, $value, $overwrite=true)
    {//...}
}

If you look at the call to

#File: lib/Varien/Simplexml/Config.php
$xml = simplexml_load_string($string, $this->_elementClass);

you can see we’re specifying the class to use (the second function parameter) with the $this->_elementClass property. You’ll may also remember this property was passed in as a second parameter of the loadString method.

#File: lib/Varien/Simplexml/Config.php
return $this->loadString($fileData, $this->_elementClass);

It looks like the core team originally intended to allow a programmer to override the base class that’s used via the method call, but failed or declined to implement this feature.

By default, the element class is a Varien_Simplexml_Element. This default is set directly on the object property in the Varien_Simplexml_Config class definition file.

#File: lib/Varien/Simplexml/Config.php
protected $_elementClass = 'Varien_Simplexml_Element';

However, if we pop-up to the Mage_Core_Model_Config_Base class’s constructor, we can see we’re setting a custom element class for our config object.

#File: app/code/core/Mage/Core/Model/Config/Base.php
public function __construct($sourceData=null)
{
    $this->_elementClass = 'Mage_Core_Model_Config_Element';
    parent::__construct($sourceData);
}

This means any simple xml object created will be a Mage_Core_Model_Config_Element, which has Varien_Simplexml_Element as an ancestor.

Class Hierarchy Review

Before we continue, we’ve just introduced a number of new classes, and it may be useful to review the hierarchy and what they’re used for.

The Mage_Core_Model_Config class, which extends Mage_Core_Model_Config_Base, which in turn extends Varien_Simplexml_Config will be the class for our Magento global configuration object. Although there’s nothing enforcing this in the system, this object should be thought of as a global and/or singleton object. Many different Magento objects may have a reference to this configuration object, but each of these references should be pointing at the same object.

The Mage_Core_Model_Config object has an _xml property. This property will store a PHP SimpleXML object which represents the tree for the Magento global config. However, this SimpleXML object is instantiated with a custom PHP class. This class is Mage_Core_Model_Config_Element, which extends a Varien_Simplexml_Element, which extends the built in SimpleXMLElement.

This hierarchy of classes may seem overly complicated, and while I’d personally agree, a key piece of the core team’s original design goals for Magento was to make the system as flexible and customizable as possible, and to push the limits of PHP 5’s often under-utilized object oriented features. It’s easy see a situation where one member of the core team though “Hey, let’s use the custom class feature of the simple xml objects”, and another member of the core team though “Configuration XML should have wrapper classes which share the same base”.

If you’re this far along you probably don’t need reminding, but it’s often necessary to put aside how you would have implemented an online retail system and accept how Magento’s been built.

Loading the Rest

Since we’re done with loadFile, let’s get back to the loadBase method

#File: app/code/core/Mage/Core/Model/Config.php
public function loadBase()
{
    $etcDir = $this->getOptions()->getEtcDir();
    $files = glob($etcDir.DS.'*.xml');

    $this->loadFile(current($files));
    while ($file = next($files)) {
        $merge = clone $this->_prototype;
        $merge->loadFile($file);
        $this->extend($merge);
    }
    if (in_array($etcDir.DS.'local.xml', $files)) {
        $this->_isLocalConfigLoaded = true;
    }
    return $this;
}

At this point, we’re about the enter the while loop. Our _xml property contain the XML tree for the file at app/etc/config.xml. This is the absolute minimum configuration Magento needs to bootstrap a basic system. The while loop will go through the remaining XML files and load each one. If you’ve ever accidentally copied another XML file into the app/etc folder and had weird things happen, this is the reason for that weirdness. Magento will load ANY file that ends in .xml.

The loading of these files gets a little tricky. Let’s take a look at the code responsible for this. First, Magento clones the object in $this->_prototype.

#File: app/code/core/Mage/Core/Model/Config.php
$merge = clone $this->_prototype;
$merge->loadFile($file);
$this->extend($merge);

If you remember back to our initial constructor, the object in _prototype was set to be an empty instance of Mage_Core_Model_Config_Base

#File: app/code/core/Mage/Core/Model/Config.php
$this->_prototype       = new Mage_Core_Model_Config_Base();

Mage_Core_Model_Config_Base is the same class that Mage_Core_Model_Config has as an ancestor. When Magento clones this object into the variable $merge, it’s making a copy, and then loading the current XML file into the new object

#File: app/code/core/Mage/Core/Model/Config.php
$merge->loadFile($file);

The core team could have just written the code like this

$merge = new Mage_Core_Model_Config_Base;
$merge->loadFile($file);

but as the configuration system was likely one of the first parts of Magento built, it seems like they were still working out what sorts of abstractions they’d be using, which partially explains this sort-of prototypical inheritance pattern for the configuration objects.

That aside, we’re finally at the really tricky part. We now have two XML config objects. There’s our Magento global config object, and this new config object. Each config object contains an individual XML tree. The question we’re now faced with is

How do we combine these two trees into a single object

The new object’s contents are merged with the first tree via the extend method.

#File: app/code/core/Mage/Core/Model/Config.php
$this->extend($merge);

If you look at the definition of the extend method

#File: lib/Varien/Simplexml/Config.php
public function extend(Varien_Simplexml_Config $config, $overwrite=true)
{
    $this->getNode()->extend($config->getNode(), $overwrite);
    return $this;
}

you can see the ultimate extend method is implemented on the custom simple XML object class, Varien_Simplexml_Element. This is also the first time we’ve encountered the getNode method. When used without a parameter, getNode returns the contents of the $this->_xml property.

#File: lib/Varien/Simplexml/Element.php    
public function getNode($path=null)
{
    if (!$this->_xml instanceof Varien_Simplexml_Element) {
        return false;
    } elseif ($path === null) {
        return $this->_xml;
    } else {
        return $this->_xml->descend($path);
    }
}

So extend could also be written as

$this->_xml->extends($config->_xml,...);

if _xml wasn’t a protected property

Extending a Varien XML Node

So what does is mean to “extend” or “merge” two XML trees in Magento? Rather than walk through the code, we’re going to describe how extend works. It’s a method that will recursively copy one xml tree into the first, (recursively!). That is, we’re extending the original XML tree with the second.

The basic metaphor is our current XML tree lacks certain nodes, so its asking the new XML tree to provide them.

Consider the following code

$first = new Mage_Core_Model_Config_Base;
$first->loadString('<config>
    <one></one>
    <two></two>
    <three></three>
</config>');

$second = new Mage_Core_Model_Config_Base;
$second->loadString('<config>
    <four></four>
    <five></five>
</config>');

$first->extend($second);

Here we’ve created one XML configuration object, and we’re extending the first with the second. If, after running the above, we called

echo $first->getNode()->asNiceXml()

we’d get output something like this

<config>
   <one/>
   <two/>
   <three/>
   <four/>
   <five/>
</config>

The extend method has merged the two trees. Now consider something like this

$first = new Mage_Core_Model_Config_Base;
$first->loadString('<config>
    <one></one>
    <two></two>
    <three></three>
</config>');

$second = new Mage_Core_Model_Config_Base;
$second->loadString('<config>
    <one>Hello</one>
    <two>Goodby</two>
</config>');

$first->extend($second);    

Here our second XML tree has nodes that conflict with the first. When merging trees like this, the second tree wins. Checking our output again

echo $first->getNode()->asNiceXml();

will show the merged tree

<config>
   <three/>
   <one>Hello</one>
   <two>Goodby</two>
</config>

However, remember that this is a recursive merge. Trees like this

$first = new Mage_Core_Model_Config_Base;
$first->loadString('<config>
    <one>
        <two>
            <three></three>
        </two>
    </one>
</config>');

$second = new Mage_Core_Model_Config_Base;
$second->loadString('<config>
    <one>
        <two>
            <four></four>
        </two>
    </one>
</config>');

$first->extend($second);    

will not replace the contents of the <two> node. Instead they’ll be merged together, like this

<config>
   <one>
      <two>
         <three/>
         <four/>
      </two>
   </one>
</config>

It’s only when there’s a depth match that one node will completely replace another. Code like this

$first = new Mage_Core_Model_Config_Base;
$first->loadString('<config>
    <one>
        <two>
            <three>Original Value</three>
        </two>
    </one>
</config>');

$second = new Mage_Core_Model_Config_Base;
$second->loadString('<config>
    <one>
        <two>
            <four></four>
            <three>New Value</three>
        </two>
    </one>
</config>');

$first->extend($second);

would give us a final tree of like this, replacing the original tree’s nodes

<config>
   <one>
      <two>
         <four/>
         <three>New Value</three>
      </two>
   </one>
</config>

That said, this “second tree wins” behavior may be avoided by using the extend method’s second $overwrite parameter

$first->extend($second, false);

When you pass this parameter in as false, you’re instructing Magento NOT to overwrite nodes in the first tree. Only new nodes from the second tree will be added, conflicts will be ignored. This means the values in the second tree are preserved.

The code that does this is worth exploring, especially if you’re interested in recursion. However, it’s beyond the scope of this series to explore that deeply.

One last note before we move on. You’ll notice that, in the extend method mentioned above on the base config class, the $overwrite property defaults to true. However, this call wraps to another extend method, defined on our custom simple xml class

#File: lib/Varien/Simplexml/Element.php
public function extend($source, $overwrite=false)
{
    if (!$source instanceof Varien_Simplexml_Element) {
        return $this;
    }

    foreach ($source->children() as $child) {
        $this->extendChild($child, $overwrite);
    }

    return $this;
}

which has a prototype that defines the default as false. So remember, if you’re working with the config objects, extend will default to overwriting nodes but if you’re working with the raw Varien_Simplexml_Element nodes, the default behavior is to skip merging nodes that conflict. If you’re going to be doing a lot of work directly with both these objects, it’s probably best to be explicit with your override parameter and not rely on the defaults.

Back to the Base

Jumping back up to our loadBase method one last time

#File: app/code/core/Mage/Core/Model/Config.php
public function loadBase()
{
    $etcDir = $this->getOptions()->getEtcDir();
    $files = glob($etcDir.DS.'*.xml');

    $this->loadFile(current($files));
    while ($file = next($files)) {
        $merge = clone $this->_prototype;
        $merge->loadFile($file);
        $this->extend($merge);
    }
    if (in_array($etcDir.DS.'local.xml', $files)) {
        $this->_isLocalConfigLoaded = true;
    }
    return $this;
}

After we’re done merging all the discovered XML files into a single base object, Magento checks if one of the files we just loaded was app/etc/local.xml

if (in_array($etcDir.DS.'local.xml', $files)) {
    $this->_isLocalConfigLoaded = true;
}

If so, it marks a property flag to note this.

#File: app/code/core/Mage/Core/Model/Config.php
$this->_isLocalConfigLoaded = true;

The local.xml file is created when you go through the installation steps after running Magento for the first time. By marking this flag as true, the core code has given Magento client developers the ability to programmatically determine if this is an “installed” system, or if it’s a system that still needs its local.xml file created.

At this point, we’ve now loaded our base Magento configuration.

Wrap Up

So that’s the first part in our series on the Magento global config. So far we’ve covered loading the base configuration tree, as well as the general mechanism Magento uses to merge multiple XML configuration trees. Although we’re several thousand words in, we’ve only scratched the surface. Next time we’ll be covering the where and how of Magento’s module configuration files, and what effect this loading has on our global configuration object.

Originally published January 16, 2012
Series NavigationMagento Configuration: Loading Declared Modules >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 16th January 2012

email hidden; JavaScript is required