标签归档:源码分析

Magento页面构建源码分析

Magento块继承层次

大部分的块都是从Mage_Core_Block_Template继承的,部分块会加入自己的抽象类(如上所示),间接继承自Mage_Core_Block_Template。

对应一个输出块,对有一个output属性,它的值应该总是toHtml,它是视图渲染的进入点。一般每个页面只有一个块有output属性,有多个块具有output属性也是允许的,这样它会把多个块分别渲染后合并在一起。如果指定了template,那么块的调用的第一个方法将是:

    public function setTemplate($template)
    {
        $this->_template = $template;
        return $this;
    }

而对应没有给出template的块,一般会实现_construct函数,里面调用setTemplate函数:

    protected function _construct()
    {
        $this->setTemplate('page/html/head.phtml');
    }

在Mage_Core_Block_Template中实现的构造函数会执行_construct()函数。

对应给定了output=toHtml的块,接下来调用它的toHtml()方法:

//Mage_Core_Block_Abstract
    final public function toHtml()
    {
        Mage::dispatchEvent('core_block_abstract_to_html_before', array('block' => $this));
		// 模块状态
        if (Mage::getStoreConfig('advanced/modules_disable_output/' . $this->getModuleName())) {
            return '';
        }
		//缓存
        $html = $this->_loadCache();
        if ($html === false) {
            $translate = Mage::getSingleton('core/translate');
            /** @var $translate Mage_Core_Model_Translate */
            if ($this->hasData('translate_inline')) {
                $translate->setTranslateInline($this->getData('translate_inline'));
            }

            $this->_beforeToHtml();
            $html = $this->_toHtml();
            $this->_saveCache($html); //保存缓存

            if ($this->hasData('translate_inline')) {
                $translate->setTranslateInline(true);
            }
        }
        $html = $this->_afterToHtml($html);

        /**
         * Check framing options
         */
        if ($this->_frameOpenTag) {
            $html = '<'.$this->_frameOpenTag.'>'.$html.'<'.$this->_frameCloseTag.'>';
        }

        /**
         * Use single transport object instance for all blocks
         */
        if (self::$_transportObject === null) {
            self::$_transportObject = new Varien_Object;
        }
        self::$_transportObject->setHtml($html); //_data[‘html’]= $html
        Mage::dispatchEvent('core_block_abstract_to_html_after',
                array('block' => $this, 'transport' => self::$_transportObject));
        $html = self::$_transportObject->getHtml();

        return $html;
    }

看起来很复杂,实际上它是间接调用了_toHtml(),注意toHtml是final方法,它不允许被覆盖,所以,自定义的实现应该放在_toHtml()方法中:

// Mage_Core_Block_Template
    protected function _toHtml()
{
		//如果没有模板,直接返回了
        if (!$this->getTemplate()) {
            return '';
        }
        $html = $this->renderView();
        return $html;
}

    public function renderView()
{
		// Mage::getBaseDir('design')获得/…/app/design
		// $this->_viewDir = /…/app/design;
        $this->setScriptPath(Mage::getBaseDir('design'));
		//$this->getTemplateFile()获取模板文件,如果找不到会逐层返回
		//frontend\base\default\template\page/3columns.phtml
        $html = $this->fetchView($this->getTemplateFile());
        return $html;
    }

这个fetchView看起来复杂,其实就是把模板文件include进来,如此而已。

    public function fetchView($fileName)
    {
        // EXTR_SKIP protects from overriding
        // already defined variables
        extract ($this->_viewVars, EXTR_SKIP); //把视图变量释放进入
        //$do = $this->getDirectOutput(); //直接输出,不缓存,总是false
		try{
            $includeFilePath = realpath($this->_viewDir . DS . $fileName);
            if (strpos($includeFilePath, realpath($this->_viewDir)) === 0 || $this->_getAllowSymlinks()) {
                include $includeFilePath;
            }
		}
	}

从这个作为入口,层层深入(一般都是一个整体模板文件,比如1列2列3列布局等):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo $this->getLang() ?>" lang="<?php echo $this->getLang() ?>">
<head>
<?php echo $this->getChildHtml('head') ?>
</head>
<body<?php echo $this->getBodyClass()?' class="'.$this->getBodyClass().'"':'' ?>>
<?php echo $this->getChildHtml('after_body_start') ?>
<div class="wrapper">
    <?php echo $this->getChildHtml('global_notices') ?>
    <div class="page">
        <?php echo $this->getChildHtml('header') ?>
        <div class="main-container col3-layout">
            <div class="main">
                <?php echo $this->getChildHtml('breadcrumbs') ?>
                <div class="col-wrapper">
                    <div class="col-main">
                        <?php echo $this->getChildHtml('global_messages') ?>
                        <?php echo $this->getChildHtml('content') ?>
                    </div>
                    <div class="col-left sidebar"><?php echo $this->getChildHtml('left') ?></div>
                </div>
                <div class="col-right sidebar"><?php echo $this->getChildHtml('right') ?></div>
            </div>
        </div>
        <?php echo $this->getChildHtml('footer') ?>
        <?php echo $this->getChildHtml('before_body_end') ?>
    </div>
</div>
<?php echo $this->getAbsoluteFooter() ?>
</body>
</html>

由于模板本身就是include进来的,那么$this当然就是指当前的块了。getChildHtml()自然也是当前块的方法,这个方法可以调用子块,继续进行下去。页面就是如此构建的。

    public function getChildHtml($name = '', $useCache = true, $sorted = false)
    {
        if ($name === '') {
        } else {
            return $this->_getChildHtml($name, $useCache);
        }
}
    protected function _getChildHtml($name, $useCache = true)
    {
        $child = $this->getChild($name);

        if (!$child) {
            $html = '';
        } else {
            $this->_beforeChildToHtml($name, $child);
            $html = $child->toHtml();
        }

        $this->_childrenHtmlCache[$name] = $html;
        return $html;
    }

看到了吧,getChildHtml()最终还是调用了块的toHtml()方法加载模板,而这个方法就是include模板文件。(当然了,务必记住,一个块是否能使用getChildHtml()把子块调进来,关键在于这个块已经定义了这个子块,否则不工作)

永久链接:http://blog.ifeeline.com/495.html
原创文章,转载务必保留出处。

Magento布局语法源码分析

在控制器方法中调用的loadLayout()方法中,最后一步是根据构建的XML内容创建布局块(generateLayoutBlocks方法),我们从源代码分析一下布局块的应该如何使用。

public function generateLayoutBlocks()
{
   	 $this->getLayout()->generateBlocks();
    	return $this;
}

// Mage_Core_Model_Layout
    public function generateBlocks($parent=null)
    {
        if (empty($parent)) {
            $parent = $this->getNode(); //$this->_xml
        }
        foreach ($parent as $node) {
            $attributes = $node->attributes(); //属性
	    //在生成xml时,已经对所有remove节点添加了ignore为true的属性
            if ((bool)$attributes->ignore) { 
                continue;
            }
	    //节点名 block reference action
            switch ($node->getName()) { 
                case 'block':
                    $this->_generateBlock($node, $parent); 
                    //$this->_blocks[$blockName] = new $className();  节点的name值只是一个_blocks数组中的下标
                    $this->generateBlocks($node);  // 递归
                    break;

                case 'reference':
                    $this->generateBlocks($node); // 递归
                    break;

                case 'action':  //通过递归,层层进入
                    $this->_generateAction($node, $parent);
                    break;
            }
        }
    }

从这个方法可以看到,如果节点名是block,就会创建这个块(_generateBlock,稍后探讨细节),然后递归(如果块还包含了块,那么继续创建这个块),如果是reference直接递归进入内层(所以内层要么是block,要么是action,因为refernce表示引用别的块,不需要做任何创建的动作)。如果是action,调用_generateAction。通过这个循环递归的过程,一个块树就建立起来了。

看看这个块树建立的逻辑:

    protected function _generateBlock($node, $parent)
    {
        //block节点 可以使用type 和 class 属性指定它的类型
        if (!empty($node['class'])) {	
            $className = (string)$node['class'];
        } else {
            $className = (string)$node['type'];
        }
        //节点name属性值
        $blockName = (string)$node['name']; 

        //$this->_blocks[$blockName] = new $className();  节点的name值只是一个_blocks数组中的下标 ***********
        $block = $this->addBlock($className, $blockName); 
        if (!$block) {
            return $this;
        }

        //用parent指定了父节点,否则采用函数传递进来的
        if (!empty($node['parent'])) {  
            $parentBlock = $this->getBlock((string)$node['parent']);
        } else {
            $parentName = $parent->getBlockName();
            
            if (!empty($parentName)) {
                $parentBlock = $this->getBlock($parentName);
            }
        }
        // 父节点不应该为空
        if (!empty($parentBlock)) { 
            //as属性记录了别名 一般和name属性一致,实际上节点插入时跟name的名字“绑定了”,如果使用上当要reference是要用name的名字,当在模板中调用getChildHtml()时,要指定as的名字(质疑这个实现)。
            $alias = isset($node['as']) ? (string)$node['as'] : '';  
            if (isset($node['before'])) {
                $sibling = (string)$node['before'];
                if ('-'===$sibling) {
                    $sibling = '';
                }
                //插入到父节点下,在$sibling前
                $parentBlock->insert($block, $sibling, false, $alias); 
            } elseif (isset($node['after'])) {
                $sibling = (string)$node['after'];
                if ('-'===$sibling) {
                    $sibling = '';
                }
		//插入到父节点下 在$sibling后
                $parentBlock->insert($block, $sibling, true, $alias);  
            } else {
                //否者就是附加
                $parentBlock->append($block, $alias); 
            }
        }
        if (!empty($node['template'])) { //如果有template,那么实际就是调用块的setTemplate()方法,为这个块分配模板
            $block->setTemplate((string)$node['template']);
        }
        //块的output属性保存了一个方法,它总是应该是toHtml,一般一个页面只有一个块有output属性,它是输出的起点。注意,这个块用_output数组保存,但是它之前已经添加到了块树中。
        if (!empty($node['output'])) {	
            $method = (string)$node['output'];
            $this->addOutputBlock($blockName, $method);
/*
    public function addOutputBlock($blockName, $method='toHtml')
   {	
	//它用了_output[块名]=array(块名,方法名)
        $this->_output[$blockName] = array($blockName, $method);
        return $this;
    }
*/
        }

        return $this;
    }

这里先看看block的语法:

name		一个在布局对象中_blocks数组的下标
as		别名,一般和name一致,refernce使用name,getChildHtml()时用as
parent		指出父块
before		在块之前的块
after		在块之后的块
template	块模板,实际调用块的setTemplate()方法
output		实际调用块的addOutputBlock方法,把output指定的值保存到一个数组,它是渲染的起点,一般都是toHtml

至于refernce,只有name属性,它必须和已经存在的块的name匹配,表示对已经存在的块的操作。

基本上,块树建立的逻辑就是如此,但是还有一个可以为action的节点,看看_generateAction方法:

    protected function _generateAction($node, $parent)
    {
        if (isset($node['ifconfig']) && ($configPath = (string)$node['ifconfig'])) {
            if (!Mage::getStoreConfigFlag($configPath)) {
                return $this;
            }
        }
	// action的method属性值
        $method = (string)$node['method'];
        // action可以使用block指出它的父块,如果没有指定,那就是直接包含它的块
        if (!empty($node['block'])) {
            $parentName = (string)$node['block'];
        } else {
            $parentName = $parent->getBlockName();
        }

        //通过名字,直接引用块树中的块
        if (!empty($parentName)) {
            $block = $this->getBlock($parentName);
        }
        if (!empty($block)) {

            $args = (array)$node->children();
            unset($args['@attributes']);
	    //循环子节点 处理子节点
            foreach ($args as $key => $arg) {
            	//每个子节点都应该是Mage_Core_Model_Layout_Element类型的实例
                if (($arg instanceof Mage_Core_Model_Layout_Element)) {
		    //子节点有包含helper属性,马上转换
                    if (isset($arg['helper'])) { 
                        //比如<url helper="customer/getLoginUrl" />
                        $helperName = explode('/', (string)$arg['helper']); 
                        $helperMethod = array_pop($helperName); //getLoginUrl
                        $helperName = implode('/', $helperName); //customer
                        $arg = $arg->asArray(); //参数
                        unset($arg['@']);
					   //$args[url]=http:://…
                        $args[$key] = call_user_func_array(array(Mage::helper($helperName), $helperMethod), $arg); 
                    } else {
                        /**
                         * if there is no helper we hope that this is assoc array
                         */
                        //处理action子节点还包含子节点的情况,一般没有这种情况
                        $arr = array();
                        foreach($arg as $subkey => $value) {
                            $arr[(string)$subkey] = $value->asArray();
                        }
                        if (!empty($arr)) {
                            $args[$key] = $arr;
                        }
                    }
                }
            }

            if (isset($node['json'])) { //action节点还可以带上一个json属性
                $json = explode(' ', (string)$node['json']);
                foreach ($json as $arg) {
                    $args[$arg] = Mage::helper('core')->jsonDecode($args[$arg]);
                }
            }

            $this->_translateLayoutNode($node, $args);
            //调用这个块的指定方法,并把子节点作为参数数组
            call_user_func_array(array($block, $method), $args); 
        }
        return $this;
    }

为了配合这段代码,先附上一个XML:

<reference name="top.links">
<action method="addLink" translate="label title" module="customer">
<label>Log In</label>
<url helper="customer/getLoginUrl"/>
<title>Log In</title>
<prepare/>
<urlParams/>
<position>100</position>
</action>
</reference>

从代码逻辑来看,action节点用method指定了将调用块的方法,用block来指定这个方法的块,用ifconfig来获取后台配置值,如果检查到值返回了假值,这个action不执行。action中的子节点都将作为你method的参数,如果子节点包含了helper,马上调用这个herper转换成键值对。子节点还可以包含子节点,它将转换成一个数组。这些子节点的顺序必须和method的参数顺序一致。比如上面的XML将转换成:

Array
(
    [label] => Log In
    [url] => http://learn.magento.com/index.php/customer/account/login/
    [title] => Log In
    [prepare] => 
    [urlParams] =>
    [position] => 100
)

这里就是布局的大部分内容。可以调整这些嵌套关系,获取不同的页面布局。

永久链接: http://blog.ifeeline.com/493.html
原创文章,转载务必保留出处。

Magento路由器源码分析

Mage_Core_Model_App的run()方法中,最后运行的是:

$this->getFrontController()->dispatch();

下面跟踪进去:

    public function getFrontController()
    {
        if (!$this->_frontController) {
            $this->_initFrontController();
        }

        return $this->_frontController;
    }
    protected function _initFrontController()
    {
        $this->_frontController = new Mage_Core_Controller_Varien_Front();
        Mage::register('controller', $this->_frontController);
        Varien_Profiler::start('mage::app::init_front_controller');
        $this->_frontController->init();
        Varien_Profiler::stop('mage::app::init_front_controller');
        return $this;
    }

实例化Mage_Core_Controller_Varien_Front,并且把它注册到注册表中(一般如果要获取前端控制器,一般用法为Mage::app()->getFrontController())。然后调用它的init方法完成初始化。

Mage_Core_Controller_Varien_Front的init方法:

    //Mage_Core_Controller_Varien_Front---->
    public function init()
    {
	//触发controller_front_init_before事件
        Mage::dispatchEvent('controller_front_init_before', array('front'=>$this));
	//getStore()返回当前使用的Store,前端控制器的XML_STORE_ROUTERS_PATH为web/routers,就从配置文件中取出这个web/routers
        $routersInfo = Mage::app()->getStore()->getConfig(self::XML_STORE_ROUTERS_PATH);
/*
$routersInfo---> 这些信息是从全局XML文件中获取的
Array
(
    [admin] => Array
        (
            [area] => admin
            [class] => Mage_Core_Controller_Varien_Router_Admin
        )

    [standard] => Array
        (
            [area] => frontend
            [class] => Mage_Core_Controller_Varien_Router_Standard
        )

)
*/
        Varien_Profiler::start('mage::app::init_front_controller::collect_routers');
	// 只有Admin 和 Standard路由器有collectRoutes方法,也只有它们需要收集路由
        foreach ($routersInfo as $routerCode => $routerInfo) {
            if (isset($routerInfo['disabled']) && $routerInfo['disabled']) {
                continue;
            }
            if (isset($routerInfo['class'])) {
		//对应生成一个路由器实例
                $router = new $routerInfo['class'];
		//如果设置了使用的区域,比如这里的admin和frontend,然后调用具体路由器的collectRoutes(收集路由)
                if (isset($routerInfo['area'])) {
		    //****  每个路由用_modules[]存储模块   _routes[]存储路由
                    $router->collectRoutes($routerInfo['area'], $routerCode); //frontend  standard
                }
		//把路由器添加进来 *******  每个路由器又有一个前端控制器的引用
                $this->addRouter($routerCode, $router);
            }
        }
        Varien_Profiler::stop('mage::app::init_front_controller::collect_routers');

	//触发事件 实际这里会添加CMS路由器(细节可以参考Magento事件实现)
        Mage::dispatchEvent('controller_front_init_routers', array('front'=>$this));

        // Add default router at the last 添加默认路由器
        $default = new Mage_Core_Controller_Varien_Router_Default();
        $this->addRouter('default', $default);

        return $this;
    }

    //Mage_Core_Controller_Varien_Router_Standard 以Standard为例子,分析collectRoutes方法
    public function collectRoutes($configArea, $useRouterName)  //frontend  standard
    {
        $routers = array();
        $routersConfigNode = Mage::getConfig()->getNode($configArea.'/routers'); //  frontend/routers
/*
<frontend>
	<routers>
		<core>
			<use>standard</use>
			<args>
				<module>Mage_Core</module>
				<frontName>core</frontName>
			</args>
		</core>
	</routers>
</frontend>
*/
        if($routersConfigNode) {
            $routers = $routersConfigNode->children();
        }
	//注意$routerName 是 这条路由信息的第一个标签,实际上是模块名
        foreach ($routers as $routerName=>$routerConfig) { 
            $use = (string)$routerConfig->use;//standard
            if ($use == $useRouterName) { //$useRouterName -> standard  路由器匹配
                $modules = array((string)$routerConfig->args->module); //模块名-> array(Mage_Core)
                if ($routerConfig->args->modules) { //一般是一个,admin路由器会执行这段代码
                    foreach ($routerConfig->args->modules->children() as $customModule) {
                        if ($customModule) {
                            if ($before = $customModule->getAttribute('before')) {
                                $position = array_search($before, $modules);
                                if ($position === false) {
                                    $position = 0;
                                }
                                array_splice($modules, $position, 0, (string)$customModule);
                            } elseif ($after = $customModule->getAttribute('after')) {
                                $position = array_search($after, $modules);
                                if ($position === false) {
                                    $position = count($modules);
                                }
                                array_splice($modules, $position+1, 0, (string)$customModule);
                            } else {
                                $modules[] = (string)$customModule;
                            }
                        }
                    }
                }

                $frontName = (string)$routerConfig->args->frontName;  //获取frontName->core
                $this->addModule($frontName, $modules, $routerName);  //添加模块  core  array(Mage_Core)   core
            }
        }
    }
    public function addModule($frontName, $moduleName, $routeName) //添加模块  core  array(Mage_Core)   core
    {
        $this->_modules[$frontName] = $moduleName;  //$this->_modules[core] = array(Mage_Core)  对应真实模块
        $this->_routes[$routeName] = $frontName;    //$this->_routes[core] = core		对应路由
        return $this;
    }
    //Mage_Core_Controller_Varien_Front---->
    public function addRouter($name, Mage_Core_Controller_Varien_Router_Abstract $router)
    {
        $router->setFront($this);  //这样,每个路由器中可以getFront获取前端控制器的引用
        $this->_routers[$name] = $router; //在前端控制器中保存了全部添加的路由器
        return $this;
    }

前端控制初始化后,接着调用它的dispatch()方法:

    public function dispatch()
    {
	//获取请求对象
        $request = $this->getRequest();

        // If pre-configured, check equality of base URL and requested URL
	//检查BaseUrl,在后台System->Configuration->Web->Url Option->Auto-redirect to Base URL
	//根据请求URL,验证是否跟设置的一样,根据验证结果是否重定向
        $this->_checkBaseUrl($request);

	//setPathInfo()在初始化请求对象时已经调用了一次,首先调用请求对象的setDispatched()标记对象未分发
        $request->setPathInfo()->setDispatched(false);
        if (!$request->isStraight()) { //$request->isStraight() 为false
            Varien_Profiler::start('mage::dispatch::db_url_rewrite');
	    //Mage_Core_Model_Url_Rewrite 通过输出请求对象可以看到它从数据库匹配请求,回写到这个对象中
            Mage::getModel('core/url_rewrite')->rewrite();  
            Varien_Profiler::stop('mage::dispatch::db_url_rewrite');
        }
        Varien_Profiler::start('mage::dispatch::config_url_rewrite');
	//从全局xml中找global/rewrite,然后循环看是否匹配 没有内容匹配 跟从数据回写一样
        $this->rewrite();
        Varien_Profiler::stop('mage::dispatch::config_url_rewrite');
        Varien_Profiler::start('mage::dispatch::routers_match');
	// 经过了以上的重写对应关系,应该已经获取了标准的URL(比如重写了的已经对应回来),下面开始路由选择
        $i = 0;
        while (!$request->isDispatched() && $i++<100) {
            foreach ($this->_routers as $router) {
                if ($router->match($this->getRequest())) {
                    break;
                }
            }
        }
        Varien_Profiler::stop('mage::dispatch::routers_match');
        if ($i>100) {
            Mage::throwException('Front controller reached 100 router match iterations');
        }
        // This event gives possibility to launch something before sending output (allow cookie setting)
        Mage::dispatchEvent('controller_front_send_response_before', array('front'=>$this));
        Varien_Profiler::start('mage::app::dispatch::send_response');
        $this->getResponse()->sendResponse(); //回送响应
        Varien_Profiler::stop('mage::app::dispatch::send_response');
        Mage::dispatchEvent('controller_front_send_response_after', array('front'=>$this));
        return $this;
    }

下面分析Standard路由器中的match方法:

    public function match(Zend_Controller_Request_Http $request)
    {
        // 检查是否是后台
        if (!$this->_beforeModuleMatch()) {
            return false;
        }

        $this->fetchDefault();
/* 设置前端控制的$this->_defaults  数组,默认模块 控制器 方法。当找不到模块 控制器 和方法时,通过getDefault($key)来查找默认

值
// $this->_defaults = array('module'=>'core','controller'=>'index','action')
    public function fetchDefault()
    {
        $this->getFront()->setDefault(array(
            'module' => 'core',
            'controller' => 'index',
            'action' => 'index'
        ));
/////    
    Mage_Core_Controller_Varien_Front
    public function setDefault($key, $value=null)
    {
        if (is_array($key)) {
            $this->_defaults = $key;
        } else {
            $this->_defaults[$key] = $value;
        }
        return $this;
    }
//////
    }
*/
        $front = $this->getFront(); //获取前端控制器
        $path = trim($request->getPathInfo(), '/');//获取路径信息 当访问首页时$path为空

	//拆分路径信息
        if ($path) {
            $p = explode('/', $path);
        } else {
            $p = explode('/', $this->_getDefaultPath());//返回cms
/*
    protected function _getDefaultPath()
    {
        return Mage::getStoreConfig('web/default/front'); //web/default/front ->cms
		
    }
*/
        }

        // get module name
        if ($request->getModuleName()) {  //先从请求对象索取模型名 当前没有设置(这里有可能在路由分发前操作请求对象)
            $module = $request->getModuleName();
        } else { // 从路径名开始索取
            if (!empty($p[0])) {   //有模块名  $p[0]一般是有值的,除非没有设置使用CMS
                $module = $p[0];
            } else {		   //如果没有设置就会进入 core 模块
                $module = $this->getFront()->getDefault('module');//取得默认的模块  module = core
                $request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS, '');
/*
    public function setAlias($name, $target)  
    {
        $this->_aliases[$name] = $target; // _aliases['rewrite_request_path']=''
        return $this;
    }
*/
            }
        }
	//如果没有找到模块,再次判断是否是在后台,如果是,模块就是admin。这段代码应该永远都不会被执行。
        if (!$module) {
            if (Mage::app()->getStore()->isAdmin()) {
                $module = 'admin';
            } else {
                return false;
            }
        }

        /**
         * Searching router args by module name from route using it as key
         */
	//这个时候获取的模块名只是前端名,要到配置中获取真实的模块名,比如core对应Mage_Core(config/frontend/routers)
        $modules = $this->getModuleByFrontName($module);
	//如果模块没有正确获取,返回进行下一个路由器匹配,模块根本不存在时匹配
        if ($modules === false) {
            return false;
        }

        // checks after we found out that this router should be used for current module
	// 模块匹配后做的事情,standard路由器中_afterModuleMatch()返回false
        if (!$this->_afterModuleMatch()) {
            return false;
        }

        /**
         * Going through modules to find appropriate controller
         */
        $found = false;
	//循环找到的模块,一般都只有一个
        foreach ($modules as $realModule) {
	    //在请求对象中设置路由名字(这个一般和模块名一致)
            $request->setRouteName($this->getRouteByFrontName($module));
/* 每个路由器处理了使用_modules数组保存模块对应的真实模块外,还使用了_routes数组保存了前端名对应的路由(用模块名标识)
    public function getRouteByFrontName($frontName)
    {
        return array_search($frontName, $this->_routes);
    }
*/
            // get controller name
	    // 还是先从请求对象中索要前端控制器名字
            if ($request->getControllerName()) {
                $controller = $request->getControllerName();
            } else {
		//路径的第二部分看做是控制器名字
                if (!empty($p[1])) {
                    $controller = $p[1];
                } else {
		    //从前端控制器获取默认控制器名 index
                    $controller = $front->getDefault('controller');
                    $request->setAlias(
                        Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,
                        ltrim($request->getOriginalPathInfo(), '/')
                    );
/*
    public function setAlias($name, $target)  
    {
        $this->_aliases[$name] = $target; // _aliases['rewrite_request_path']=ltrim($request->getOriginalPathInfo(), '/') 

覆盖了前面的设置  就是原始URL 比如index.php/hello/index -> hello/index
        return $this;
    }
*/
                }
            }

            // get action name
            if (empty($action)) {
		//还是先向请求对象索要方法名,不行就从路径中获取,否则就是默认的(index)
                if ($request->getActionName()) {
                    $action = $request->getActionName();
                } else {
                    $action = !empty($p[2]) ? $p[2] : $front->getDefault('action');
                }
            }

            //checking if this place should be secure
	    // '/core/index/index'
            $this->_checkShouldBeSecure($request, '/'.$module.'/'.$controller.'/'.$action);
/*
    protected function _checkShouldBeSecure($request, $path = '')
    {
        if (!Mage::isInstalled() || $request->getPost()) {
            return;
        }
	//如果给定的路径是必须使用安全连接的,但是当前发起的请求不是安全连接
        if ($this->_shouldBeSecure($path) && !$request->isSecure()) {
            $url = $this->_getCurrentSecureUrl($request); //生成安全连接
///////////////
    protected function _getCurrentSecureUrl($request)
    {
        if ($alias = $request->getAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS)) {
	    //从配置从获取安全的BaseUrl,然后加上$alias(这是一个原始请求URI)
            return Mage::getBaseUrl('link', true).ltrim($alias, '/');
        }

        return Mage::getBaseUrl('link', true).ltrim($request->getPathInfo(), '/');
    }
///////////////
	    // 如果路由是adminhtml 并且 在URL中使用Session,获取重定位的新URL
            if ($request->getRouteName() != 'adminhtml' && Mage::app()->getUseSessionInUrl()) {
                $url = Mage::getSingleton('core/url')->getRedirectUrl($url);
            }
	    // 发送重定向
            Mage::app()->getFrontController()->getResponse()
                ->setRedirect($url)
                ->sendResponse();
            exit;
        }
    }
*/
            $controllerClassName = $this->_validateControllerClassName($realModule, $controller);
/*
    protected function _validateControllerClassName($realModule, $controller) //Mage_Core   index
    {
	//获取控制器文件名
        $controllerFileName = $this->getControllerFileName($realModule, $controller);

/*start getControllerFileName
    public function getControllerFileName($realModule, $controller)
    {
        $parts = explode('_', $realModule);
        $realModule = implode('_', array_splice($parts, 0, 2)); //array_splice 返回删除的数组
        $file = Mage::getModuleDir('controllers', $realModule);
////////////
    public static function getModuleDir($type, $moduleName)
    {
        return self::getConfig()->getModuleDir($type, $moduleName); //最终在配置文件和Mage_Core_Model_Config_Options对象的

辅助下获取这个模块控制器目录名
		//比如 Mage_Core  ->   /.../app/core/Mage/Core/controllers
    }
////////////		
        if (count($parts)) { //注意$parts已经被删除了第一和第二部分,实际这里为空
            $file .= DS . implode(DS, $parts);
        }
		// /.../app/core/Mage/Core/controllers/indexController.php
        $file .= DS.uc_words($controller, DS).'Controller.php';
        return $file;
    }
//end getControllerFileName

	//验证控制器文件  比如是否可写
        if (!$this->validateControllerFileName($controllerFileName)) {
            return false;
        }

        $controllerClassName = $this->getControllerClassName($realModule, $controller);
/////////////////
    public function getControllerClassName($realModule, $controller)
    {
        $class = $realModule.'_'.uc_words($controller).'Controller';//这个就是为何控制器名字要写带上模块名字的原因
        return $class;
    }
/////////////////
        if (!$controllerClassName) {
            return false;
        }

        // include controller file if needed
	//手动包含控制器文件
        if (!$this->_includeControllerClass($controllerFileName, $controllerClassName)) {
            return false;
        }

        return $controllerClassName;
    }
*/
//从以上分析来看,这里有一个疑问,为何控制器的类不是自动装载的?为了回答这个问题,首先看看如果实例化一个模型,比如

Mage::getModel('core/website'),首先到XML中搜索到global/modules/core,然后获取类名为Mage_Core_Model,然后组成

Mage_Core_Model_Website,然后直接new它,类文件就能自动装载了。但是对于控制器,首先是没有配置,然后是类的命名并没有对应于路径

,比如Mage_Core_IndexController,它是放入到Mage/Core/controllers/IndexController.php中,如果直接new 

Mage_Core_IndexController它根本无法找到。在Zend Framework中,控制器的名字是直接命名的,比如直接叫IndexController,只要放入

到对应文件夹即可,因为控制器的类文件根本不是自动加载的。但是在Magento的实现中,控制器的类名需要带上一个模块名前缀。
            if (!$controllerClassName) {
                continue;
            }

            // instantiate controller class
	    // 注意看,实例化一个控制器时,请求对象和响应对象都送入到了它构造函数中
            $controllerInstance = Mage::getControllerInstance($controllerClassName, $request, $front->getResponse());

            if (!$controllerInstance->hasAction($action)) {
                continue;
            }

            $found = true;
            break;
        }

        /**
         * if we did not found any suitable
         */
        if (!$found) {
            if ($this->_noRouteShouldBeApplied()) { //$this->_noRouteShouldBeApplied()在Standard中硬编码为false
                $controller = 'index';
                $action = 'noroute';

                $controllerClassName = $this->_validateControllerClassName($realModule, $controller);
                if (!$controllerClassName) {
                    return false;
                }

                // instantiate controller class
                $controllerInstance = Mage::getControllerInstance($controllerClassName, $request,
                    $front->getResponse());

                if (!$controllerInstance->hasAction($action)) {
                    return false;
                }
            } else {
                return false;
            }
        }

        // set values only after all the checks are done
        $request->setModuleName($module);
        $request->setControllerName($controller);
        $request->setActionName($action);
        $request->setControllerModule($realModule);

        // set parameters from pathinfo
	// 查询参数
        for ($i = 3, $l = sizeof($p); $i < $l; $i += 2) {
            $request->setParam($p[$i], isset($p[$i+1]) ? urldecode($p[$i+1]) : '');
        }

        // dispatch action
        $request->setDispatched(true);
	//控制器分发
        $controllerInstance->dispatch($action);

        return true;
    }

接下来看看路由器类的继承关系:
Magneto路由器继承

Mage_Core_Controller_Varien_Router_Abstract要求实现match()方法。Mage_Cms_Controller_Router 和 Mage_Core_Controller_Varien_Router_Default也只实现了match方法。大部分的功能是在Mage_Core_Controller_Varien_Router_Standard类中提供的,而Mage_Core_Controller_Varien_Router_Admin继承自它,主要覆盖了collectRoutes方法(意思是,路由匹配方法match还是使用从standard继承过来的方法)。

在standard路由器的match方法中可以看到:

        // dispatch action
        $request->setDispatched(true);
        $controllerInstance->dispatch($action);

但是Mage_Cms_Controller_Router 和 Mage_Core_Controller_Varien_Router_Default的match中就没有这两行代码了。这意味着,如果这两个路由器匹配,外层循环继续,最终要么匹配admin路由器,要么匹配standard路由。

实际上,如果访问首页,在Standard路由器中将被修改为使用CMS路由处理:

<frontend>
<routers>
<cms>
<use>standard</use>
<args>
<module>Mage_Cms</module>
<frontName>cms</frontName>
</args>
</cms>
</routers>
</frontend>

如果访问具体的CMS页面,cms路由器将匹配,但是它只是告诉请求对象如下信息:

        $request->setModuleName('cms')
            ->setControllerName('page')
            ->setActionName('view')
            ->setParam('page_id', $pageId);

然后外层循环继续,再次到达Standard路由器时匹配而进行路由分发的。而对于经过了admin standard 和 cms路由器的匹配后,最终没有匹配,那么default路由器就会匹配。它的match方法异常简单:

    public function match(Zend_Controller_Request_Http $request)
    {
        $noRoute        = explode('/', Mage::app()->getStore()->getConfig('web/default/no_route'));
/*        
 <default>
        	<cms_home_page>home</cms_home_page>
        	<cms_no_route>no-route</cms_no_route>
        	<cms_no_cookies>enable-cookies</cms_no_cookies>
        	<front>cms</front>
        	<no_route>cms/index/noRoute</no_route>
        	<show_cms_breadcrumbs>1</show_cms_breadcrumbs>
 	</default>
 		模块名被修改为了CMS 对应index控制器  和 noRoute方法,noRoute方法就是处理404返回的逻辑
*/
        
        $moduleName     = isset($noRoute[0]) ? $noRoute[0] : 'core';
        $controllerName = isset($noRoute[1]) ? $noRoute[1] : 'index';
        $actionName     = isset($noRoute[2]) ? $noRoute[2] : 'index';

        if (Mage::app()->getStore()->isAdmin()) {
            $adminFrontName = (string)Mage::getConfig()->getNode('admin/routers/adminhtml/args/frontName');
            if ($adminFrontName != $moduleName) {
                $moduleName     = 'core';
                $controllerName = 'index';
                $actionName     = 'noRoute';
                Mage::app()->setCurrentStore(Mage::app()->getDefaultStoreView());
            }
        }

        $request->setModuleName($moduleName)
            ->setControllerName($controllerName)
            ->setActionName($actionName);

        return true;
    }

它会定位到cms/index/noRoute。这个大部分是404的出现了。 接下来继续匹配到Standard路由器,然后才被分发的。回顾一下标准路由器中如何获取模块和控制器:

        // get module name
        if ($request->getModuleName()) {
            $module = $request->getModuleName();
        }
// get controller name
       	if ($request->getControllerName()) {
           	$controller = $request->getControllerName();
        	}

这段代码此时应该非常明白了。

路由器的实现是Magento中核心的关键部分之一。保持一个清晰的认识十分有必要。

永久链接:http://blog.ifeeline.com/470.html
原创文章,转载务必保留出处。

Magento源码分析索引

在index.php文件中,首先运行的是Mage::run()方法。

self::$_app    = new Mage_Core_Model_App();
self::$_events = new Varien_Event_Collection();
self::_setConfigModel($options); // self::$_config =  new Mage_Core_Model_Config($options);
self::$_app->run(array(
 	'scope_code' => $code,
  	'scope_type' => $type,
  	'options'    => $options,
));

主要代码就这些,得到了一个$_app、$_events、$_config,可以看到大量地方出现$options这个变量,默认是空的,它的作用主要是传递一个配置数组,这样可以修改使用默认的对象,比如配置对象,可以通过$options[‘config_model’]传入自定义的配置模型。

接下来就进入了Mage_Core_Model_App类的run方法。但是Mage类还是提供了一系列的有用的方法。

    public static function reset()
    {
        self::$_registry        = array();
        self::$_appRoot         = null;
        self::$_app             = null;
        self::$_config          = null;
        self::$_events          = null;
        self::$_objects         = null;
        self::$_isDownloader    = false;
        self::$_isDeveloperMode = false;
        self::$_isInstalled     = null;
        // do not reset $headersSentThrowsException
    }

很多方法都是直接或间接围绕这些变量展开的。比如register和unregister方法提供了把变量注册到$_registry或把变量从$_registry中删除,registry通过键获取注册表中值。

以下是两个重要的工厂方法:

public static function getModel($modelClass = '', $arguments = array())
{
return self::getConfig()->getModelInstance($modelClass, $arguments);
}

    public static function helper($name)
    {
        $registryKey = '_helper/' . $name;
        if (!self::registry($registryKey)) {
            $helperClass = self::getConfig()->getHelperClassName($name);
            self::register($registryKey, new $helperClass);
        }
        return self::registry($registryKey);
    }

如下的调用应该不陌生:

$blogpost = Mage::getModel('weblog/blogpost'); 

它实际调用的是配置对象的getModelInstance方法,它首先在配置对象中寻找weblog节点,找到了对应的class子节点取出值为Vfeelit_Weblog_Model,然后把blogpost附加上去返回Vfeelit_Weblog_Model_Blogpost,然后实例化这个类,类文件怎么加载?把它转换成Vfeelt/Weblog/Model/Blogpost.php文件是也(自动装载)。 helper函数也差不多。(这里通过配置文件获取完整类名,但是令人困惑的是,只要提供完整类名,类完全是可以自动装载的,这里非要绕个弯,而且weblog/blogpost难道比Vfeelit_Weblog_Model_Blogpost更加具有优势?不见得吧,只是短了一点而已,并且需要把对应信息写入XML的配置文件中。 据说Magento 2使用这个做法)

紧接着应用就伴随着Mage_Core_Model_App对象继续深入。同样,看看它的run()方法:

    public function run($params)
    {
        $options = isset($params['options']) ? $params['options'] : array();
		//基础配置文件加载 仅仅是app/etc下的local.xml和config.xml文件
        $this->baseInit($options);
        Mage::register('application_params', $params);

        if ($this->_cache->processRequest()) {
            $this->getResponse()->sendResponse();
        } else {
			//加载模块配置,app/etc/moules所有文件和每个模块的etc中的config.xml文件,还包括从数据库中加载配置进来
            $this->_initModules();
            $this->loadAreaPart(Mage_Core_Model_App_Area::AREA_GLOBAL, Mage_Core_Model_App_Area::PART_EVENTS);

            if ($this->_config->isLocalConfigLoaded()) {
                $scopeCode = isset($params['scope_code']) ? $params['scope_code'] : '';
                $scopeType = isset($params['scope_type']) ? $params['scope_type'] : 'store';
				//当前商店初始化
                $this->_initCurrentStore($scopeCode, $scopeType);
				//请求初始化
                $this->_initRequest();
                Mage_Core_Model_Resource_Setup::applyAllDataUpdates();
            }
			//前端控制器分发,后续进入路由分发
            $this->getFrontController()->dispatch();
        }
        return $this;
    }

调用了baseInit()进行一些基本的初始化。接着就是看是否命中缓存,如果命中就直接返回了,否则就开始初始化模块,初始化当前商店,初始化请求,运行模块自动安装更新脚本,然后获取前端控制器后进行分发。

接下来分
1 Magento的配置对象与全局XML配置文件构建
2 Magento如何从数据库加载配置信息合并到全局XML
3 Magento店铺初始化(布局XML文件构建)
4 Magento的请求初始化
5 Magento的路由分发
6 …..

先这些吧。可能写不完。

永久连接: http://blog.ifeeline.com/408.html