- The Magento Global Config, Revisited
- Magento Configuration: Loading Declared Modules
- The Magento Config: Loading System Variables
- Magento Config: A Critique and Caching
This article is part of a longer series exploring the Magento global configuration object. While this article contains useful stand-alone information, you’ll want to read part 1 and part 2 of the series to fully understand what’s going on.
When our last article finished up, we had successfully loaded a list of every declared module, as well as each of those module’s config.xml
files into the main global configuration object’s root node. It seemed like our job was done. However, there’s one last step that need to be taken, and before we can talk about that, we need to talk about Magento’s system configuration variables
System Configuration
If you’ve spent, or are going to spend, a significant time around Magento, you need to get used to a certain amount of flexibility around definitions. So far this series of articles has talked about “Magento Configuration”, where we’re defining Magento Configuration as the config.xml
files added to each individual module. Things get tricky when you consider the other types of configuration that are available and necessary in Magento.
For example, all those layout XML files are often considered configuration, but they have almost nothing to do with the global configuration we’ve been talking about. So both systems are configuration, but they’re still two separate systems.
Another such system is the one I’ve been calling Magento’s system configuration variables. If you’re familiar with the system you know it’s a way to quickly configure a user interface for entering values that a Magento module developer may want a Magento user to change.
Once setup, a Magento module developer can make a call like this
Mage::getStoreConfig('foo/baz/bar');
to fetch one of those configuration values. This is most often used to create on/off settings for features, store changeable strings, etc. If you’ve dug into this system you know after saving a value via the Magento admin interface, it’s persisted to the core_config_data
database table.
You may also know, in the abstract, that directly changing these values in the database table isn’t enough to change a configuration value. You also need to clear Magento’s cache.
Another seemingly weird bit is the default values are not stored in the database, or in the system.xml
files used to configure the interface. Instead, module developers add a <default/>
node to the global configuration tree via a module’s config.xml
file. Values stored here become the default. The core_config_data
table only stores values that are explicitly set, along with the scope they’re set at (default, websites, or store).
To a newcomer this system seems cumbersome, hard to understand, and hard to explain to others. That’s because there’s one key assumption that’s not immediately obvious.
That assumption is this: The ultimate destination and “source of truth” for configuration values is the global configuration tree. The core_config_data
table is simply a persistence way-station for these user set, scoped values. When you’re reading out a value with getStoreConfig
, Magento is looking at the global configuration object.
Starting with Default
With that in mind, let’s pretend we’re developing a configuration system from scratch. Assuming a bare, un-scoped system of key/value lookups, we might say something like
Alright, let’s create a top level node in our configuration tree, and store values there
This node is the <default/>
node. At this point, we’re rather pleased with ourselves, and start storing all sorts of useful configuration values. After working with such a system for a bit, we quickly discover a pattern develops where specific websites and/or stores need mostly the same configuration values, with one or two exceptions. Rather than duplicate every set of configuration values, we decide a scoped configuration system is in order.
That is, we want to be able to say all nodes at foo/baz/*
have a particular value, except for foo/baz/bar
which has a value of XXXX in this store, YYYY in this website, and ZZZZ everywhere else. Since we’ve already decided all configuration values should live in the global configuration tree, we quickly add two new top level nodes
<config>
<websites>
</websites>
<stores>
</stores>
</config>
to store values with the scope of <websites/>
and <stores/>
. Once again, we’re very pleased with ourselves, and continue coding away on our store.
While this purely XML based configuration system works great for us, (the developers), non-technical end-users are having all sorts of trouble. It’s decided a user interface is needed to allow users to set their own custom values. As developers, we’re game to create this system, but the idea of a system that’s writing directly to config.xml
files sets off all sorts of red flags. A simple persistence bug could destroy the entire module configuration, and a clever exploit of the bug could give malicious users the ability to rewrite the configuration of modules however they saw fit.
So, because of this, our system configuration editing system will work like this
- Users will be able to set values for any configuration node at any scope, but these values will be persisted elsewhere (the
core_config_data
table) - When loading the global config, we’ll look at this elsewhere (
core_config_data
) and load any new values into the global config
From an outside point of view, it’s easy to scoff at the complexity involved, but if you consider the system from the Varien/Magento point of view, it’s easier to understand how it came to be. Modern agile software development practices rarely leave time to consider the system as a whole; modern systems are more the results of a set of individual decisions made in haste over a period of time, and then working around the unintended consequences.
Loading the Variables
So, now that we have a little background on the why, we can look at how Magento loads the system configuration variables into the global configuration tree.
Taking a look at our Magento application object’s _initModules
method
#File: app/code/core/Mage/Core/Model/App.php
protected function _initModules()
{
if (!$this->_config->loadModulesCache()) {
$this->_config->loadModules();
if ($this->_config->isLocalConfigLoaded() && !$this->_shouldSkipProcessModulesUpdates()) {
Varien_Profiler::start('mage::app::init::apply_db_schema_updates');
Mage_Core_Model_Resource_Setup::applyAllUpdates();
Varien_Profiler::stop('mage::app::init::apply_db_schema_updates');
}
$this->_config->loadDb();
$this->_config->saveCache();
}
return $this;
}
the call we’re interested in is
#File: app/code/core/Mage/Core/Model/App.php
$this->_config->loadDb();
The loadDb
method is the one that kicks off loading system configuration variables from the database into the global configuration tree. You’ll recall from earlier articles the call to
#File: app/code/core/Mage/Core/Model/App.php
$this->_config->loadModules();
is where we loaded our module’s config.xml
files. So, this means any values hard coded into a config.xml
file’s <default/>
, <websites/>
, or <stores/>
node are already loaded in the global config.
You may also be interest in the call to
#File: app/code/core/Mage/Core/Model/App.php
Mage_Core_Model_Resource_Setup::applyAllUpdates();
which precedes the loadDb
method. This static method call is what kicks of the running of the setup resource scripts (Magento’s version of migrations). We’re not going to go into the how of this, but you should be aware that since we haven’t yet loaded the system configuration variables into the tree, that checking the value of any configuration field from a setup resource script won’t get you the results you want.
Hopping to the config object’s class,
#File: app/code/core/Mage/Core/Model/Config.php
public function loadDb()
{
if ($this->_isLocalConfigLoaded && Mage::isInstalled()) {
Varien_Profiler::start('config/load-db');
$dbConf = $this->getResourceModel();
$dbConf->loadToXml($this);
Varien_Profiler::stop('config/load-db');
}
return $this;
}
we can the see the loadDb
method gets a reference to a resource model object, and then calls it’s loadToXml
method, passing the configuration object in as $this
#File: app/code/core/Mage/Core/Model/Config.php
$dbConf = $this->getResourceModel();
$dbConf->loadToXml($this);
The loadToXml
method is where the actual database work of the loading is done. Since the configuration object passes itself in as $this
, it’s a safe bet that the actual merging of the configuration will be done in this method as well.
The Configuration Resource Model
So which resource model is it? If we look at the getResourceModel
definition, we can see
#File: app/code/core/Mage/Core/Model/Config.php
public function getResourceModel()
{
if (is_null($this->_resourceModel)) {
$this->_resourceModel = Mage::getResourceModel('core/config');
}
return $this->_resourceModel;
}
that the resource model is hard coded, and is instantiated with a getResourceModel
method. This resolves to the Mage_Core_Model_Resource_Config
class, which is located at
app/code/core/Mage/Core/Model/Resource/Config.php
Let’s take a look at the loadToXml
method.
Loading the XML
The first thing to pay attention to is the method prototype.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
public function loadToXml(Mage_Core_Model_Config $xmlConfig, $condition = null)
{
...
}
This method accepts only a config object that is, or inherits from, Mage_Core_Model_Config
. In our case, this is the global configuration object that was passed in as $this
from above. The loadToXml
method is going to manipulate this object, and load in any system configuration values it finds in the core_config_data
table.
Of course, to do this we’ll need a way to read database values
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$read = $this->_getReadAdapter();
if (!$read) {
return $this;
}
The _getReadAdapter
method is a standard method on every model resource object, and will fetch you a direct handler to the database. This allows you to create SQL queries via a standard Zend style interface. With a read adapter in hand, we’re ready to query the database. The first query is this chunk of code
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$websites = array();
$select = $read->select()
->from($this->getTable('core/website'), array('website_id', 'code', 'name'));
$rowset = $read->fetchAssoc($select);
Here Magento is directly querying the database with SQL that looks something like this
SELECT website_id, code, name FROM core_website
The core_website
table is where Magento persists core/website
models, created via the admin console at
System -> Manage Stores
There’s various reasons the core team may be using raw SQL here instead of the model API. As always, the best point of view is to be curious about why something was done, but the accept the pattern even if you disagree with it.
After running the above query, each row is iterated over
#File: app/code/core/Mage/Core/Model/Resource/Config.php
foreach ($rowset as $w) {
$xmlConfig->setNode('websites/'.$w['code'].'/system/website/id', $w['website_id']);
$xmlConfig->setNode('websites/'.$w['code'].'/system/website/name', $w['name']);
$websites[$w['website_id']] = array('code' => $w['code']);
}
and used to set a new node on our configuration object’s global tree. The end result is a tree that looks something like this
<config>
<!-- ... -->
<websites>
<admin>
<system>
<website>
<id>0</id>
</website>
</system>
</admin>
<base>
<system>
<website>
<id>1</id>
</website>
</system>
</base>
<other_website_code>
<system>
<website>
<id>2</id>
</website>
</system>
</other_website_code>
</websites>
</config>
with each sub-node of <websites/>
representing the information loaded from the database. With this information in the tree, any Magento system or client developer now has access to all the website codes (and their database IDs) loaded in the system.
In addition to creating a permeant record of this information in the global configuration tree, there’s also this line
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$websites[$w['website_id']] = array('code' => $w['code']);
which stashes the website code into a local array. We’ll be seeing this variable again later, but it’s safe to ignore for now.
Loading the Stores
Next up we see a very similar pattern, but this time it applies to core/store
objects (also managed at System -> Manage Stores
)
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$stores = array();
$select = $read->select()
->from($this->getTable('core/store'), array('store_id', 'code', 'name', 'website_id'))
->order('sort_order ' . Varien_Db_Select::SQL_ASC);
$rowset = $read->fetchAssoc($select);
foreach ($rowset as $s) {
if (!isset($websites[$s['website_id']])) {
continue;
}
$xmlConfig->setNode('stores/'.$s['code'].'/system/store/id', $s['store_id']);
$xmlConfig->setNode('stores/'.$s['code'].'/system/store/name', $s['name']);
$xmlConfig->setNode('stores/'.$s['code'].'/system/website/id', $s['website_id']);
$xmlConfig->setNode('websites/'.$websites[$s['website_id']]['code'].'/system/stores/'.$s['code'], $s['store_id']);
$stores[$s['store_id']] = array('code'=>$s['code']);
$websites[$s['website_id']]['stores'][$s['store_id']] = $s['code'];
}
The SQL query that’s run here is
SELECT store_id, code, name, website_id FROM core_store;
Again, rows are iterated over to create a node for each store
<config>
<!-- ... -->
<stores>
<default>
<system>
<store>
<id>[STORE_ID]</id>
</store>
<website>
<id>[WEBSITE_ID]</id>
</website>
</system>
</default>
<!-- ... above node structure repeat for each store row ... -->
</stores>
</config>
You’ll also notice the following line
#File: app/code/core/Mage/Core/Model/Resource/Config.php $xmlConfig->setNode('websites/'.$websites[$s['website_id']]['code'].'/system/stores/'.$s['code'], $s['store_id']);
which dives back into the <websites/>
node to set store codes and ids under each website-code node.
<!-- ... -->
<websites>
<!-- ... -->
<other_website_code>
<system>
<website>
<id>2</id>
</website>
<!-- START NEW NODES -->
<stores>
<store_code>[STORE_ID]</store_code>
</stores>
<!-- END NEW NODES -->
</system>
</other_website_code>
</websites>
This duplication of information is likely a bit of legacy code. While the Magento system developers could easily fix thier own code to only look in the one true place for a piece of information, they can’t go and fix all the code in the world that was written against early versions of Magento.
Back to the code at hand, you’ll also see that we’re stashing this information away in local $stores
and $websites
variables again
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$stores[$s['store_id']] = array('code'=>$s['code']);
$websites[$s['website_id']]['stores'][$s['store_id']] = $s['code'];
Loading the Persisted Configuration Values
Next up, we’re finally set to load the configuration values from the core_config_data
table.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$substFrom = array();
$substTo = array();
// load all configuration records from database, which are not inherited
$select = $read->select()
->from($this->getMainTable(), array('scope', 'scope_id', 'path', 'value'));
if (!is_null($condition)) {
$select->where($condition);
}
$rowset = $read->fetchAll($select);
The query we’re constructing above looks something like this
SELECT scope, scope_id, path, value FROM core_config_data;
Also, notice that we’re using the $condition
variable to create a WHERE
clause. This variable is the loadXml
method’s second parameter. In our case it’s not used, and is either legacy or a paramater used internally by the core team during development.
Also, ignore the $substFrom
and $substTo
variables for now, we’ll get to them eventually.
We now have an array of results in $rowset
, with each inner array structured as follows
array(
'scope' => 'default',
'scope_id' => '0',
'path' => 'web/unsecure/base_url',
'value' => 'http://magento1point6point1.dev/'
);
Each of these inner arrays represent one persisted system configuration variable from the admin console. The rest of the loadXml
method is taking this information and loading it into the global configuration tree.
Loading Default Scoped Configuration Variables
Next up is the following
#File: app/code/core/Mage/Core/Model/Resource/Config.php
// set default config values from database
foreach ($rowset as $r) {
if ($r['scope'] !== 'default') {
continue;
}
$value = str_replace($substFrom, $substTo, $r['value']);
$xmlConfig->setNode('default/' . $r['path'], $value);
}
Here Magento is looping through all the rows returned from the core_config_data
table. However, notice the conditional guard clause
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if ($r['scope'] !== 'default') {
continue;
}
Magento will skip any iteration of this loop that doesn’t have 'default'
as a scope value. That’s because the purpose of this loop is to set only the values for the <default/>
node. This is done with a relatively straight forward call to setNode
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$xmlConfig->setNode('default/' . $r['path'], $value);
Here Magento uses the string path stored with each system configuration variable to set a specific node in the global configuration tree. Using x-ray variable vision, that would look something like
$xmlConfig->setNode('default/web/unsecure/base_url', 'http://store.example.com/');
The call to setNode
will replace any value previously set.
Prior to calling setNode
, there’s this curious call
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$value = str_replace($substFrom, $substTo, $r['value']);
Here Magento is using the $substFrom
and $substTo
arrays to transform the configuration value after loading it from the database table. What’s curious about this is $substFrom
and $substTo
are empty arrays, and will always be empty arrays. It’s not clear if this was the start of some configuration substitution system, of if it’s an internal convention used during development and/or testing. Regardless, you can safely ignore this call.
We now have a fully loaded <default/>
node, with values from the database superseding values set directly in the configuration. However, we’re not done with the default node yet.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$extendSource = $xmlConfig->getNode('default');
foreach ($websites as $id=>$w) {
$websiteNode = $xmlConfig->getNode('websites/' . $w['code']);
$websiteNode->extend($extendSource);
}
Remember the information we were stashing earlier in the $websites
array? Here’s where we’re using it. Magento will iterate over each website id/code pair, and then use it to fetch the entire website
node from global config tree. The, our old friend extend is used to copy and merge the contents of the <default/>
node into each individual <websites/>
node. This means each individual node at websites/[CODE]
has a full copy of every configuration value in the system.
Loading Website Scoped Configuration Variables
So, we now have a loaded <default/>
configuration node. We also have a <websites/>
configuration node loaded with all the values from <default/>
(via the extend
method). Our next step is to load the values from core_config_data
with website scope into the <websites/>
node.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$deleteWebsites = array();
// set websites config values from database
foreach ($rowset as $r) {
if ($r['scope'] !== 'websites') {
continue;
}
$value = str_replace($substFrom, $substTo, $r['value']);
if (isset($websites[$r['scope_id']])) {
$nodePath = sprintf('websites/%s/%s', $websites[$r['scope_id']]['code'], $r['path']);
$xmlConfig->setNode($nodePath, $value);
} else {
$deleteWebsites[$r['scope_id']] = $r['scope_id'];
}
}
Here we’re looping through our $rowset
again, except this time we’re skipping anything that doesn’t have a website scope
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if ($r['scope'] !== 'websites') {
continue;
}
Another difference is we’re checking for the existence of a scope_id
key in the $websites
array before setting our node
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if (isset($websites[$r['scope_id']])) {
$nodePath = sprintf('websites/%s/%s', $websites[$r['scope_id']]['code'], $r['path']);
$xmlConfig->setNode($nodePath, $value);
} else {
$deleteWebsites[$r['scope_id']] = $r['scope_id'];
}
The plain english meaning of this if
clause is
Does the website this configuration variable was set for still exist? If so, set the value, if not, stash the website id in the new
$deleteWebsites
array.
Another slight difference here is the construction of the node path
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$nodePath = sprintf('websites/%s/%s', $websites[$r['scope_id']]['code'], $r['path']);
This code constructs a path in the form
websites/[WEBSITE CODE]/[PERSISTED/VARIABLE/PATH]
When we were setting the <default/>
we didn’t need the additional website code
node, as there’s only one level of default values.
Loading Store Scoped Configuration Variables
With our <websites/>
node fully populated, it’s time to load the values into the <stores/>
node. The first step towards this is to copy and merge information from each <websites/>
node into a corresponding <stores/>
node. That’s done with the following code.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
// extend website config values to all associated stores
foreach ($websites as $website) {
$extendSource = $xmlConfig->getNode('websites/' . $website['code']);
if (isset($website['stores'])) {
foreach ($website['stores'] as $sCode) {
$storeNode = $xmlConfig->getNode('stores/'.$sCode);
/**
* $extendSource DO NOT need overwrite source
*/
$storeNode->extend($extendSource, false);
}
}
}
For each id/code pair in $websites
, this loop will get a reference to the tree of just set configuration variables
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$extendSource = $xmlConfig->getNode('websites/' . $website['code']);
Then, if we previously determined this website had stores
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if (isset($website['stores'])) {
...
}
we’ll iterate over each of the stores within that website
#File: app/code/core/Mage/Core/Model/Resource/Config.php
foreach ($website['stores'] as $sCode) {
$storeNode = $xmlConfig->getNode('stores/'.$sCode);
/**
* $extendSource DO NOT need overwrite source
*/
$storeNode->extend($extendSource, false);
}
and use the node from <websites/>
as the source node for the extend
copy and merge into the stores/[STORE_CODE]
node. Much like <websites/>
started with a full copy of <default/>
, each sub-code node in <stores/>
starts will a full copy of its parent website values.
With this base in place, it’s time to loop through $rowset
one more time
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$deleteStores = array();
// set stores config values from database
foreach ($rowset as $r) {
if ($r['scope'] !== 'stores') {
continue;
}
$value = str_replace($substFrom, $substTo, $r['value']);
if (isset($stores[$r['scope_id']])) {
$nodePath = sprintf('stores/%s/%s', $stores[$r['scope_id']]['code'], $r['path']);
$xmlConfig->setNode($nodePath, $value);
} else {
$deleteStores[$r['scope_id']] = $r['scope_id'];
}
}
This loop is identical to the loop for “websites
” scoped values, except that we’re only looking at “stores
” scoped values
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if ($r['scope'] !== 'stores') {
continue;
}
and setting nodes under the <stores/>
node.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$nodePath = sprintf('stores/%s/%s', $stores[$r['scope_id']]['code'], $r['path']);
and stashing the IDs of stores that don’t exist in $deleteStores
#File: app/code/core/Mage/Core/Model/Resource/Config.php
$deleteStores[$r['scope_id']] = $r['scope_id'];
At this point we now have a global configuration tree that has all possible configuration values loaded: Both those configured directly in config.xml
, and values persisted to the core_config_data
table by the UI. Despite being done, there’s one last bit of housekeeping that needs to happen.
Self Cleaning
While we were inserting values into the <websites/>
and <stores/>
nodes, we were also populating two arrays
$deleteWebsites
$deleteStores
As a reminder (in case you’re as frazzled reading all that as we are writing it), these arrays contain store and website IDs that were encountered in the scope_id
column, but whose value didn’t match any website or store object in the system. The reason these values were being stashed is the last bit of code in the loadXml
method.
#File: app/code/core/Mage/Core/Model/Resource/Config.php
if ($deleteWebsites) {
$this->_getWriteAdapter()->delete($this->getMainTable(), array(
'scope = ?' => 'websites',
'scope_id IN(?)' => $deleteWebsites,
));
}
if ($deleteStores) {
$this->_getWriteAdapter()->delete($this->getMainTable(), array(
'scope=?' => 'stores',
'scope_id IN(?)' => $deleteStores,
));
}
These two blocks of code each construct a DELETE
SQL query to remove any configuration records that match the IDs stored in the two arrays.
DELETE FROM core_config_data WHERE scope_id IN (7,8,9);
DELETE FROM core_config_data WHERE scope_id IN (6,4,7);
We’ll be covering rationales for this sort of code in a future article, so for now just be aware it happens, and make sure you never remove a store or website record from the database if there’s important information stored in that store or website’s configuration!
Using the System Configuration Variables
Since we’ve come this far, it’s worth investigating how the system configuration system fetches its values. Right now we’ve loaded values into three top level nodes
<default/>
<websites/>
<stores/>
but it’s not clear which of these is the “source of truth” for a particular value. That’s where the Mage::getStoreConfig
method comes into play. It acts as a single point of entry for Magento client developers to retrieve any configuration value by its three part path
Mage::getStoreConfig('foo/baz/bar);
This static method is defined on the final Mage
class
#File: app/Mage.php
public static function getStoreConfig($path, $store = null)
{
return self::app()->getStore($store)->getConfig($path);
}
Let’s break out the method chaining to make this code easier to talk about. The above could be rewritten as
$app = self::app();
$store_object = $app->getStore($store);
$value = $store->getConfig($path);
return $value;
When you call getStoreConfig
, Magento gets a reference to an instance of the specific store object you’ve passed in as the second parameter. If this value is omitted (as is usually the case), getStore
returns an instance of the current store object. Then, the passed in $path
is handed off to the store object’s getConfig
method. Let’s take a look at that method
#File: app/code/core/Mage/Core/Model/Store.php
public function getConfig($path)
{
if (isset($this->_configCache[$path])) {
return $this->_configCache[$path];
}
$config = Mage::getConfig();
$fullPath = 'stores/' . $this->getCode() . '/' . $path;
$data = $config->getNode($fullPath);
if (!$data && !Mage::isInstalled()) {
$data = $config->getNode('default/' . $path);
}
if (!$data) {
return null;
}
return $this->_processConfigValue($fullPath, $path, $data);
}
This is where the majority of the work retrieving a configuration value is done. To start, the store object checks it’s local _configCache
for a value and returns it
#File: app/code/core/Mage/Core/Model/Store.php
if (isset($this->_configCache[$path])) {
return $this->_configCache[$path];
}
This saves PHP from re-running the code to fetch a config value it’s already retrieved once. Next up,
#File: app/code/core/Mage/Core/Model/Store.php
$config = Mage::getConfig();
$fullPath = 'stores/' . $this->getCode() . '/' . $path;
$data = $config->getNode($fullPath);
Here Magento gets a reference to the global configuration tree object with Mage::getConfig
. Then, using the passed in configuration path and the store code of the current object, constructs the following path
stores/[CODE HERE]/foo/baz/baz
and then uses that path to retrieve some configuration data via getNode
. Next up is this bit of code
#File: app/code/core/Mage/Core/Model/Store.php
if (!$data && !Mage::isInstalled()) {
$data = $config->getNode('default/' . $path);
}
if (!$data) {
return null;
}
If Magento fails to load data at the specified path, AND determines Magento hasn’t been installed yet, it will search for values in the <default/>
node. At this point if Magento still hasn’t found a value it gives up, returning null
.
So, before we continue, it’s worth noting that despite loading the entire <websites/>
node into the configuration object, Magento never consults this node when loading a configuration value. That said, website
scope still works as each store is, by definition, part of a website.
Finally, rather than return a value directly, the store object’s getConfig
method passes the value into _processConfigValue
before returning the requested value to the end user.
#File: app/code/core/Mage/Core/Model/Store.php
return $this->_processConfigValue($fullPath, $path, $data);
The _processConfigValue
method is where the raw configuration node is turned into a concrete value for the Magento client programmer. Let’s take a look.
Processing Configuration Values
The _processConfigValue
method starts off by also checking the local _configCache
for a value.
#File: app/code/core/Mage/Core/Model/Store.php
protected function _processConfigValue($fullPath, $path, $node)
{
if (isset($this->_configCache[$path])) {
return $this->_configCache[$path];
}
...
}
This is a redundant check in this particular code path, but a little paranoid programming never hurt anyone. Next, if the fetched data/node has any child nodes
#File: app/code/core/Mage/Core/Model/Store.php
if ($node->hasChildren()) {
$aValue = array();
foreach ($node->children() as $k => $v) {
$aValue[$k] = $this->_processConfigValue($fullPath . '/' . $k, $path . '/' . $k, $v);
}
$this->_configCache[$path] = $aValue;
return $aValue;
}
then Magento will run though each child, make a single recursive call to _processConfigValue
, and store the results in an array. This array is then cached to the local _configCache
property, and then the array is returned to the end user. This is what allows you to fetch all the configuration values under a particular node namespace with code like the following
$array = Mage::getStoreConfig('foo/baz');
$array = Mage::getStoreConfig('foo');
Assuming we’re dealing with a concrete, childless value node, there’s two major tasks the _processConfigValue
method needs to accomplish. First, the Magento configuration supports a special attribute on config nodes named backend_model
. This attribute allows a developer to programmatically manipulate a configuration value before returning it. The second bit of processing that needs to be done is replacing certain {{template}} {{values}}
.
Let’s take a look at both in turn
Magento’s Backend Model Configuration Processing
The processing of the backend_model
attribute happens in this code block
#File: app/code/core/Mage/Core/Model/Store.php
$sValue = (string) $node;
if (!empty($node['backend_model']) && !empty($sValue)) {
$backend = Mage::getModel((string) $node['backend_model']);
$backend->setPath($path)->setValue($sValue)->afterLoad();
$sValue = $backend->getValue();
}
First, Magento casts the node as a string to fetch its value. Then, it searches the uncast $node
for a backend model attribute. You can see an example of this in the Mage_Usa
module’s config.xml
.
<config>
<!-- ... -->
<default>
<!-- ... -->
<carriers>
<dhl>
<id backend_model="adminhtml/system_config_backend_encrypted"/>
</dhl>
</carriers>
</default>
</config>
Remember, the nodes in <default/>
are extended
into both the <websites/>
and <stores/>
nodes, so even though the config.xml
only has this attribute in the <default/>
node, it will carry over to the nodes under <websites/>
and <stores/>
.
This attribute contains a Magento class alias, which is used to instantiate a model.
#File: app/code/core/Mage/Core/Model/Store.php
$backend = Mage::getModel((string) $node['backend_model']);
Then, both the config value’s path and fetched value are set as data properties of that model, and then the model’s afterLoad
method is called.
#File: app/code/core/Mage/Core/Model/Store.php
$backend->setPath($path)->setValue($sValue)->afterLoad();
The implicit contract here is the model’s afterLoad
method may manipulate the value, which is fetched back out
#File: app/code/core/Mage/Core/Model/Store.php
$sValue = $backend->getValue();
From what I’ve seen, the only use of this by the core system is to process item values through the adminhtml/system_config_backend_encrypted
model’s afterLoad
method to ensure user-programmers can fetch the unencrypted value without storing it unencrypted in the database or various config caches.
Template Variable Replacements
Once Magento has processed any backend_model
attributes, the last step in processing the configuration value is replacing a set of configuration template variables.
#File: app/code/core/Mage/Core/Model/Store.php
if (is_string($sValue) && strpos($sValue, '{{') !== false) {
if (strpos($sValue, '{{unsecure_base_url}}') !== false) {
$unsecureBaseUrl = $this->getConfig(self::XML_PATH_UNSECURE_BASE_URL);
$sValue = str_replace('{{unsecure_base_url}}', $unsecureBaseUrl, $sValue);
} elseif (strpos($sValue, '{{secure_base_url}}') !== false) {
$secureBaseUrl = $this->getConfig(self::XML_PATH_SECURE_BASE_URL);
$sValue = str_replace('{{secure_base_url}}', $secureBaseUrl, $sValue);
} elseif (strpos($sValue, '{{base_url}}') === false) {
$sValue = Mage::getConfig()->substDistroServerVars($sValue);
}
}
The outer conditional block determines if template replacements need to happen by checking if the string starts with two curly brackets ({{
).
The first two conditional leafs of the inner conditional handle {{unsecure_base_url}}
and {{secure_base_url}}
as a special case. If these are present Magento will fetch another configuration value directly from the config to use in a string substitution at the following paths.
#File: app/code/core/Mage/Core/Model/Store.php
const XML_PATH_UNSECURE_BASE_URL = 'web/unsecure/base_url';
const XML_PATH_SECURE_BASE_URL = 'web/secure/base_url';
A quick warning: If you set your web/unsecure/base_url
value to {{unsecure_base_url}}
, you’ll create an endless recursion loop that will kill your entire system. So don’t do that.
The final if
clause is a little tricker
#File: app/code/core/Mage/Core/Model/Store.php
} elseif (strpos($sValue, '{{base_url}}') === false) {
$sValue = Mage::getConfig()->substDistroServerVars($sValue);
}
This code is actually doing two things. The first is, if the string {{base_url}}
is encountered, it will be left alone, unchanged. Secondly, the substDistroServerVars
method handles the final substitution of Magento’s other template variables.
Distro Server Vars
The final template variables replaced in the config are
{{root_dir}}, {{app_dir}}, {{var_dir}}, {{base_url}}
If you take a look at the substDistroServerVars
method
#File: app/code/core/Mage/Core/Model/Store.php
public function substDistroServerVars($data)
{
$this->getDistroServerVars();
return str_replace(
array_keys($this->_substServerVars),
array_values($this->_substServerVars),
$data
);
}
you’ll see this list of substitution variables and values it loaded in the getDistroServerVars
method. If we look at that method
#File: app/code/core/Mage/Core/Model/Store.php
public function getDistroServerVars()
{
if (!$this->_distroServerVars) {
if (isset($_SERVER['SCRIPT_NAME']) && isset($_SERVER['HTTP_HOST'])) {
$secure = (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS']!='off')) || $_SERVER['SERVER_PORT']=='443';
$scheme = ($secure ? 'https' : 'http') . '://' ;
$hostArr = explode(':', $_SERVER['HTTP_HOST']);
$host = $hostArr[0];
$port = isset(
$hostArr[1]) && (!$secure && $hostArr[1]!=80 || $secure && $hostArr[1]!=443
) ? ':'.$hostArr[1] : '';
$path = Mage::app()->getRequest()->getBasePath();
$baseUrl = $scheme.$host.$port.rtrim($path, '/').'/';
} else {
$baseUrl = 'http://localhost/';
}
$options = $this->getOptions();
$this->_distroServerVars = array(
'root_dir' => $options->getBaseDir(),
'app_dir' => $options->getAppDir(),
'var_dir' => $options->getVarDir(),
'base_url' => $baseUrl,
);
foreach ($this->_distroServerVars as $k=>$v) {
$this->_substServerVars['{{'.$k.'}}'] = $v;
}
}
return $this->_distroServerVars;
}
we see that the values for {{root_dir}}
, {{app_dir}}
, and {{var_dir}}
are loaded from the the options passed into the configuration object at instantiation time, while {{base_url}}
comes from transformations performed against PHP’s $_SERVER
super global. Of course, as previously discussed, {{base_url}}
is skipped in our particular code path, so it’s unclear if this is a bit of legacy code, or if there’s other parts of the core system still relying on this method for other things.
Finally, our value fully processed, it’s stashed in the local _configCache
array to ensure further requests for the saved value will be returned immediately.
#File: app/code/core/Mage/Core/Model/Store.php
$this->_configCache[$path] = $sValue;
Wrap Up
And that is how Magento loads it’s system configuration variables into the Magento global configuration object, and how those values are pulled back out. Because of the general complexity and verboseness of the code at this level, we tried to stay strictly focused on the how rather than the why of specific implementation details. A full critique of the configuration loading, along with information about it’s caching is what’s coming up next time.
Until then, don’t forget to checkout the Pulse Storm store, or catch up one the countless other Magento tutorials available at this site.