Magento Application Initialisation Part 2

Welcome to Magento Application Initialisation Part 2. This post continues on from Application Initialisation Part 1.

Th next method to look at is _initModules().

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 first line that we should be looking at is $this->_config->loadModules().

public function loadModules()
{
    Varien_Profiler::start('config/load-modules');
    $this->_loadDeclaredModules();

    $resourceConfig = sprintf('config.%s.xml', $this->_getResourceConnectionModel('core'));
    $this->loadModulesConfiguration(array('config.xml',$resourceConfig), $this);

    /**
     * Prevent local.xml directives overwriting
     */
    $mergeConfig = clone $this->_prototype;
    $this->_isLocalConfigLoaded = $mergeConfig->loadFile($this->getOptions()->getEtcDir().DS.'local.xml');
    if ($this->_isLocalConfigLoaded) {
        $this->extend($mergeConfig);
    }

    $this->applyExtends();
    Varien_Profiler::stop('config/load-modules');
    return $this;
}

So the first step is that we are sent to the _loadDeclaredModules() method which then makes a call to the _getDeclaredModuleFiles() method.

$moduleFiles = $this->_getDeclaredModuleFiles();
if (!$moduleFiles) {
    return ;
}
protected function _getDeclaredModuleFiles()
{
    $etcDir = $this->getOptions()->getEtcDir();
    $moduleFiles = glob($etcDir . DS . 'modules' . DS . '*.xml');

    if (!$moduleFiles) {
        return false;
    }

    $collectModuleFiles = array(
        'base'   => array(),
        'mage'   => array(),
        'custom' => array()
    );

    foreach ($moduleFiles as $v) {
        $name = explode(DIRECTORY_SEPARATOR, $v);
        $name = substr($name[count($name) - 1], 0, -4);

        if ($name == 'Mage_All') {
            $collectModuleFiles['base'][] = $v;
        } else if (substr($name, 0, 5) == 'Mage_') {
            $collectModuleFiles['mage'][] = $v;
        } else {
            $collectModuleFiles['custom'][] = $v;
        }
    }

    return array_merge(
        $collectModuleFiles['base'],
        $collectModuleFiles['mage'],
        $collectModuleFiles['custom']
    );
}

Firstly we can see that Magento globs up the list of xml files that we have in the app/etc/modules directory.

We then set a $collectModuleFiles array that contains three arrays: base, mage and custom. The module xml files will be sorted into these arrays as we’ll see below.

foreach ($moduleFiles as $v) {
    $name = explode(DIRECTORY_SEPARATOR, $v);
    $name = substr($name[count($name) - 1], 0, -4);

    if ($name == 'Mage_All') {
        $collectModuleFiles['base'][] = $v;
    } else if (substr($name, 0, 5) == 'Mage_') {
        $collectModuleFiles['mage'][] = $v;
    } else {
        $collectModuleFiles['custom'][] = $v;
    }
}

So from what we can gather above.

If the module file name is Mage_All, we add the file to the base array.
If the module file name starts with Mage_, we add the file to the mage array.
If the module file name is anything else, we add the file to the custom array.

We then merge these three arrays together using array_merge().

return array_merge(
    $collectModuleFiles['base'],
    $collectModuleFiles['mage'],
    $collectModuleFiles['custom']
);

Heading back to _loadDeclaredModules(), we then foreach around our $moduleFiles array and merge the contents into a merged XML configuration tree.

// load modules declarations
foreach ($moduleFiles as $file) {
    $fileConfig->loadFile($file);
    $unsortedConfig->extend($fileConfig);
}

We then need to sort the modules configuration tree by the modules depends tag. As Module B could depend on Module A being loaded, we need to ensure that Module A loads before Module B.

So first we build an array that contains the module name, the depends value, and the active value.

$moduleDepends = array();
foreach ($unsortedConfig->getNode('modules')->children() as $moduleName => $moduleNode) {
    if (!$this->_isAllowedModule($moduleName)) {
        continue;
    }

    $depends = array();
    if ($moduleNode->depends) {
        foreach ($moduleNode->depends->children() as $depend) {
            $depends[$depend->getName()] = true;
        }
    }
    $moduleDepends[$moduleName] = array(
        'module'    => $moduleName,
        'depends'   => $depends,
        'active'    => ('true' === (string)$moduleNode->active ? true : false),
    );
}

Now that the $moduleDepends array contains the information we need, we can finally sort out the module dependencies.

// check and sort module dependence
$moduleDepends = $this->_sortModuleDepends($moduleDepends);
protected function _sortModuleDepends($modules)
{
    foreach ($modules as $moduleName => $moduleProps) {
        $depends = $moduleProps['depends'];
        foreach ($moduleProps['depends'] as $depend => $true) {
            if ($moduleProps['active'] && ((!isset($modules[$depend])) || empty($modules[$depend]['active']))) {
                Mage::throwException(
                    Mage::helper('core')->__('Module "%1$s" requires module "%2$s".', $moduleName, $depend)
                );
            }
            $depends = array_merge($depends, $modules[$depend]['depends']);
        }
        $modules[$moduleName]['depends'] = $depends;
    }
    $modules = array_values($modules);

    $size = count($modules) - 1;
    for ($i = $size; $i >= 0; $i--) {
        for ($j = $size; $i < $j; $j--) {
            if (isset($modules[$i]['depends'][$modules[$j]['module']])) {
                $value       = $modules[$i];
                $modules[$i] = $modules[$j];
                $modules[$j] = $value;
            }
        }
    }

    $definedModules = array();
    foreach ($modules as $moduleProp) {
        foreach ($moduleProp['depends'] as $dependModule => $true) {
            if (!isset($definedModules[$dependModule])) {
                Mage::throwException(
                    Mage::helper('core')->__('Module "%1$s" cannot depend on "%2$s".', $moduleProp['module'], $dependModule)
                );
            }
        }
        $definedModules[$moduleProp['module']] = true;
    }

    return $modules;
}

So now that we have our sorted our module dependencies, we now create a new config object that will contain our module information. Then we append any nodes within the module declaration file that does not equal “modules”:

$sortedConfig = new Mage_Core_Model_Config_Base();
$sortedConfig->loadString('<config><modules/></config>');

foreach ($unsortedConfig->getNode()->children() as $nodeName => $node) {
    if ($nodeName != 'modules') {
        $sortedConfig->getNode()->appendChild($node);
     }
}

We then foreach over our sorted depends array and append the information into a modules node, and lastly, we merge this information into main global configuration.

foreach ($moduleDepends as $moduleProp) {
    $node = $unsortedConfig->getNode('modules/'.$moduleProp['module']);
    $sortedConfig->getNode('modules')->appendChild($node);
}

$this->extend($sortedConfig);

Arriving back at the loadModules() method, we haven’t yet done anything with the modules’ config.xml files.

The method involved is the loadModulesConfiguration().

public function loadModulesConfiguration($fileName, $mergeToObject = null, $mergeModel=null)
{
    $disableLocalModules = !$this->_canUseLocalModules();

    if ($mergeToObject === null) {
        $mergeToObject = clone $this->_prototype;
        $mergeToObject->loadString('<config/>');
    }
    if ($mergeModel === null) {
        $mergeModel = clone $this->_prototype;
    }
    $modules = $this->getNode('modules')->children();
    foreach ($modules as $modName=>$module) {
        if ($module->is('active')) {
            if ($disableLocalModules && ('local' === (string)$module->codePool)) {
                continue;
            }
            if (!is_array($fileName)) {
                $fileName = array($fileName);
            }

            foreach ($fileName as $configFile) {
                $configFile = $this->getModuleDir('etc', $modName).DS.$configFile;
                if ($mergeModel->loadFile($configFile)) {

                    $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_GLOBAL, $mergeModel);
                    $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_FRONTEND, $mergeModel);
                    $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_ADMIN, $mergeModel);
                    $this->_makeEventsLowerCase(Mage_Core_Model_App_Area::AREA_ADMINHTML, $mergeModel);

                    $mergeToObject->extend($mergeModel, true);
                }
            }
        }
    }
    return $mergeToObject;
}

The configuration found in the module’s config.xml files are merged into the config tree. It’s worth noting that we can see four areas of configuration: global, frontend, admin and adminhtml.

You will see these area nodes defined in the config.xml files.

Lastly, the local.xml file gets loaded again to prevent any configuration within the file from being overwritten.

/**
 * Prevent local.xml directives overwriting
 */
$mergeConfig = clone $this->_prototype;
$this->_isLocalConfigLoaded = $mergeConfig->loadFile($this->getOptions()->getEtcDir().DS.'local.xml');
if ($this->_isLocalConfigLoaded) {
    $this->extend($mergeConfig);
}

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