跨境电商
经验交流分享

Magento路由分发过程解析(二):Standard路由对象

本文主要关注Magento的standard路由对象中的Mage_Core_Controller_Varien_Router_Standard::match()方法,该方法在前端控制器中调用,主要用来检查当前请求的URL地址,并决定匹配的模块,控制器以及方法,并且最后调用控制器分发该方法。

对于在上篇文章前端控制器循环所有的路由器来说,该方法完成了以下任务,

  • 路由对象提供match()方法,并检测请求对象,如果匹配,则该路由对象获取该请求。
  • 将请求标记为已分发。
  • 设置请求对象。

假设没有找到匹配的模块/控制器/方法,该方法则返回false,前端控制器对象中的循环指向下一个路由对象,并调用其match()方法。

下面开始分析match()方法。该方法的参数为请求对象,继承自Zend_Controller_Request_Http。

[php]

if (!$this->_beforeModuleMatch()) {
return false;
}
[/php]

Mage_Core_Controller_Varien_Router_Standard::_beforeModuleMatch()方法用于检测当前storeID是否为0,在APP模型中,管理员界面被硬编码为storeID为0(const ADMIN_STORE_ID = 0;)。可以对比下standard与admin路由对象中该方法的不同之处。

[php]

//Mage_Core_Controller_Varien_Router_Standard::_beforeModuleMatch()

protected function _beforeModuleMatch()
{
if (Mage::app()->getStore()->isAdmin()) {
return false;
}
return true;
}

//Mage_Core_Controller_Varien_Router_Admin::_beforeModuleMatch

protected function _beforeModuleMatch()
{
return true;
}
[/php]

代码很简单明了,但是我还是不太理解这里的含义,首先admin路由对象是第一个循环,假如匹配了,则之后的代码不在运行。假如没有匹配,在standard路由对象中再次检测是否在管理员界面就没有意义了。先放在这里,回头研究。

[php]
$this->fetchDefault();
$front = $this->getFront();
[/php]

先看getFront()方法,路由器对象获取了前端控制器的引用。该方法在路由抽象类中定义。那么,还记得路由器对象是如何获取前端控制器引用的吗?在前端控制器的addRouters()方法中,路由对象通过setFront()获取了前端控制器的引用,如果忘记了,看下第一篇。在某些情况下,路由对象需要询问前端控制器对象系统默认的控制器和方法是什么。默认情况下,分别是index,index,core。

那么fetchDefault()又是神马东西呢?该方法主要是引用前端控制器对象,并设置默认的模块,控制器及方法名。

[php]
public function fetchDefault()
{
$this->getFront()->setDefault(array(
‘module’ => ‘core’,
‘controller’ => ‘index’,
‘action’ => ‘index’
));
}
[/php]

这两个方法呢,实际上各自都很容易理解,但是一上一下就不是太容易理解了。总之,这里引用了前端控制器,并设置了系统默认的模块,控制器,方法。

接下来,match()方法像请求对象询问请求的路径信息,并根据顺序添加到$p变量中。

[php]
/* <code>/catalog/category/view/id/8</code> */
$path = trim($request->getPathInfo(), ‘/’);
if ($path) {
$p = explode(‘/’, $path);
} else {
$p = explode(‘/’, $this->_getDefaultPath());
}
[/php]

在Magento中,不会使用超全局变量去获取路径信息,Magento将其抽象为请求对象,这样可以让我们集中精力处理路由任务,而不是如何去处理路径信息。

如果没有获取到$path变量的话,则使用_getDefaultPath()方法,如下所示,

[php]
//Mage_Core_Controller_Varien_Router_Standard::_getDefaultPath()
protected function _getDefaultPath()
{
return Mage::getStoreConfig(‘web/default/front’);
}

[/php]

从该路径可以轻松在后台找到系统配置的位置,

System -> Configuration -> Web -> Default Pages -> Default Web URL

如果没有更改默认值cms的话,上面获取返回的默认路径的$p变量应该有下值,

array(1) { [0]=> string(3) “cms” }

这里也可以说明,为什么Magento会将首页指向Cms模块index控制器的index方法了。

获取模块名(前端命名frontName)

到此,我们已经获取到了路径信息,应该可以从路径中获取模块了。下面这段代码主要负责获取模块名。

[php]
if ($request->getModuleName()) {
$module = $request->getModuleName();
} else {
if (!empty($p[0])) {
$module = $p[0];
} else {
$module = $this->getFront()->getDefault(‘module’);
$request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,    ”);
}
}
if (!$module) {
if (Mage::app()->getStore()->isAdmin()) {
$module = ‘admin’;
} else {
return false;
}
}
[/php]

将上述代码分开来看,首先看第一段,$request是从前端控制器中传递给路由的请求对象,我们这里尝试询问请求对象来获取模块。那么问题来了,在上面的代码当中,我们已经获取到$p路径信息,为什么不从$p中直接获取模块呢?实际上,在一个普通的请求对象中是不包含模块名这个属性的。不过,绑定Magento系统的一些事件,可能会在第一个路由对象调用其match()方法之前,处理请求对象,并添加模块属性到请求对象当中。这个过程在默认路由促发请求模块产生404页面的时候有所表现,我们会在后面的文章中深入了解这块内容。

接着看中间那段代码,

[php]
if (!empty($p[0])) {
$module = $p[0];
} else {
$module = $this->getFront()->getDefault(‘module’);
$request->setAlias(Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,    ”);
}
[/php]

如果$p中包含模块路径,则使用它。但是,假如在$p中没有获取到模块名,Magento会使用之前代码中提到的,在前端控制器中设置的默认模块。这种情况只发生于下列路径没有设置值的情况下。

System -> Configuration -> Web -> Default Pages -> Default Web URL

然后是最后一段,如果依然没有找到$module,我们需要再进行一次硬编码的判断,在standard标准路由中,这里肯定就会返回false了。如果这里是在admin路由中,则获取到模块为admin。

[php]
if (!$module) {
if (Mage::app()->getStore()->isAdmin()) {
$module = ‘admin’;
} else {
return false;
}
[/php]

获取模块全名

这里我们已经获取到了模块名(前端命名frontName),实际上,如果仔细了解Magento配置文件的话,我们会发现,在路径中$p[0]显示的实际上是配置文件里frontName节点的值。frontName一般情况下可以默认使用模块名称,但是它并不一定就是模块名。所以现在需要获取模块全名,例如上面获取到的frontName是catalog,这里全名就是Mage_Catalog。当然,这只适用于frontName与模块名相同的情况下。

于是就有了下面这段代码中的getModuleByFrontName()方法,即通过前端命名获取模块名。

[php]
$modules = $this->getModuleByFrontName($module);

//再看下这个方法的定义
public function getModuleByFrontName($frontName)
{
if (isset($this->_modules[$frontName])) {
return $this->_modules[$frontName];
}
return false;
}
[/php]

好的,非常简单的一个方法,那么问题是_modules属性是什么?从哪里被赋值的?$_modules属性是路由对象的一个属性,声明为数组,默认为空。数组键为前端命名frontName,数组值为相对应的模块全名(可能包含多个,以数组组织)。在前端控制器中遍历路由对象的时候,在实例化路由对象之后,立刻调用了该路由对象的collectRoutes()方法,在该方法的最后,调用了addModule()方法,$_modules属性就是在这里被赋值的。

[php]
public function addModule($frontName, $moduleName, $routeName)
{
//$frontName是前端命名,配置文件中的frontName节点值
$this->_modules[$frontName] = $moduleName;
//$routeName是路由信息模块区分节点,<catalog>,即根据模块的路由信息区分节点,获取前端命名
$this->_routes[$routeName] = $frontName;
return $this;
}
[/php]

那么我们现在暂时先离开match()方法内部,跳到collectRoutes()方法看下这个方法对于路由对象的作用。

Collecting the Routes

collectRoutes($configArea, $useRouterName)方法从代码上来分析,主要用来给$_modules和$_routes属性赋值。首先,根据传入的参数,$configArea,frontend或admin,遍历config.xml配置文件中的/routers节点,如下代码,

[php]
$routers = array();
$routersConfigNode = Mage::getConfig()->getNode($configArea.’/routers’);
if($routersConfigNode) {
$routers = $routersConfigNode->children();
}
[/php]

通过Mage::getConfig()->getNode()方法,$routerConfigNode获取到了config.xml配置文件中所有模块的路由信息,这里有必要复习下模块配置文件里对于路由配置的相关代码。

[xml]
<frontend>
<routers>
<ships>
<use>standard</use>
<args>
<module>Angrybats_Ships</module>
<frontName>ships</frontName>
</args>
</ships>
</routers>
</frontend>
[/xml]

这是一个典型的模块路由配置的代码,在系统运行时,全局config.xml文件会集合所有模块的配置文件,并根据节点进行整理。那么Mage::getConfig()->getNode()方法根据参数($configArea.’/routers’)即可判断是读取admin还是frontend下的routers节点内的路由配置文件了。如果获取到了该值,通过children()读取该节点下的所有路由配置信息,并赋值给$routers。

在相应的路由对象中,获取到各自的路由配置信息之后,需要将其赋值给该路由对象的$_modules及$_routes属性,这部分主要由以下代码完成,

[php]
foreach ($routers as $routerName=>$routerConfig) {
$use = (string)$routerConfig->use;
//根据当前的路由代码,添加所有匹配的模块到$modules数组中
if ($use == $useRouterName) {
$modules = array((string)$routerConfig->args->module);
if ($routerConfig->args->modules) {
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;
$this->addModule($frontName, $modules, $routerName);
}
}
[/php]

这里,将对应路由对象的路由配置信息读取完毕之后,全部赋予$modules数组。最后,通过addModule()方法,将路由对象对应的所有模块和路由信息分别添加到属性$_modules和$_routes中。一定要将这两个属性好好记住。

继续Mage_Core_Controller_Varien_Router_Standard::match()方法

了解上述内容之后,我们继续上面说到的那段代码,如果没有找到匹配的模块,尝试最后一次进行模块匹配。如下代码。如果依然没有找到,则返回false,前端控制器中的路由器迭代进入下一个路由对象。

[php]
$modules = $this->getModuleByFrontName($module);

/**
* If we did not found anything  we searching exact this module
* name in array values
*/
if ($modules === false) {
if ($moduleFrontName = $this->getModuleByName($module, $this->_modules)) {
$modules = array($module);
$module = $moduleFrontName;
} else {
return false;
}
}
[/php]

获取控制器

一切正常的话,我们已经获取到了候选的模块,接下来该获取控制器进行分发了。Magento会循环候选模块的数组(通常情况下只有一个,例如array(‘Mage_Catalog’))。

[php]
#File: app/code/core/Mage/Core/Controller/Varien/Router/Standard.php
foreach ($modules as $realModule) {
$request->setRouteName($this->getRouteByFrontName($module));

}

[/php]

在该循环中,第一件事情便是通过设置路由配置信息来操作请求对象。看下getRouteByFrontName()方法,该方法通过$_routes属性获取到路由配置信息的路由节点名称。

[php]
public function getRouteByFrontName($frontName)
{
return array_search($frontName, $this->_routes);
}
[/php]

需要复习下的是,路由配置信息名是config.xml文件中<routers>的直接子节点。获取路由配置信息名之后,一起来看下请求对象中的setRouteName()方法。

[php]
public function setRouteName($route)
{
$this->_route = $route;
$router = Mage::app()->getFrontController()->getRouterByRoute($route);
if (!$router) return $this;
$module = $router->getFrontNameByRoute($route);
if ($module) {
$this->setModuleName($module);
}
return $this;
}
[/php]

可以看到,除了给请求对象的$_route属性赋值以外,Magento还通过前端控制器中的getRouterByRoute()方法,获取当前路由对象,并根据路由配置信息名获取前端命名赋值给请求对象的$_module属性。

那么这里的$_module指的是什么呢?通过编辑器查看setModuleName()方法可以发现,该方法实际上是在请求对象的父类Zend_Controller_Request_Abstract中定义的。也就是说,这个$_module是Zend框架的模块名,而不是Magento中定义的模块名。

继续match()方法寻找控制器的代码,如下,

[php]
// get controller name
if ($request->getControllerName()) {
$controller = $request->getControllerName();
} else {
if (!empty($p[1])) {
$controller = $p[1];
} else {
$controller = $front->getDefault(‘controller’);
$request->setAlias(
Mage_Core_Model_Url_Rewrite::REWRITE_REQUEST_PATH_ALIAS,ltrim($request->getOriginalPathInfo(), ‘/’));
}
}
[/php]

这段代码似曾相识吧?和上面查询模块名的代码十分相似。首先,向请求对象查询控制器(如果说在其它地方设置了自定义控制器的话)。如果没有,在$p中查找。假如在这里也没有获取到控制器,则通过前端控制器获取默认的控制器,默认情况下,为core模块的index控制器。

接下来,相似的代码,用来获取控制器方法,

[php]
// get action name
if (empty($action)) {
if ($request->getActionName()) {
$action = $request->getActionName();
} else {
$action = !empty($p[2]) ? $p[2] : $front->getDefault(‘action’);
}
}
[/php]

去除复杂部分,总结下上述获取模块,控制器,控制器方法的一个相同模式,

  • 首先询问请求对象
  • 接着询问$p数组变量
  • 最后询问前端控制器默认设置

验证并获取类及相关文件

到这里,我们已经获取到了控制器及方法名。接下来,在当前模块内查找是否存在和控制其与方法名相对应的控制器类文件。如下代码,

[php]
$controllerClassName = $this->_validateControllerClassName($realModule, $controller);
//根据上面获取到的数据,这行代码类似如下形式
$controllerClassName = $this->_validateControllerClassName(‘Mage_Catalog’, ‘category’);
[/php]

通过该方法的名字可以很容易了解它的作用,该方法根据参数(模块和控制器),生成类文件名及类名,并验证该类文件中是否真实定义了这个类。一起来看下它的定义,

[php]
/**
* Generating and validating class file name,
* class and if evrything ok do include if needed and return of class name
*
* @return mixed
*/
protected function _validateControllerClassName($realModule, $controller)
{
$controllerFileName = $this->getControllerFileName($realModule, $controller);
if (!$this->validateControllerFileName($controllerFileName)) {
return false;
}

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

// include controller file if needed
if (!$this->_includeControllerClass($controllerFileName, $controllerClassName)) {
return false;
}

return $controllerClassName;
}
[/php]

在该方法内部,通过另外三个方法,首先获取并验证控制器文件名;

[php]
$controllerFileName = $this->getControllerFileName($realModule, $controller);
if (!$this->validateControllerFileName($controllerFileName)) {
return false;
}

//看下getControllerFileName()方法
/**
* 返回控制器文件名
*
* @param string $realModule 例如:Mage_Catalog
* @param string $controller 例如:category
* @return string $file 控制器文件名绝对路径
*/
public function getControllerFileName($realModule, $controller)
{
$parts = explode(‘_’, $realModule);
$realModule = implode(‘_’, array_splice($parts, 0, 2));
$file = Mage::getModuleDir(‘controllers’, $realModule);
if (count($parts)) {
$file .= DS . implode(DS, $parts);
}
$file .= DS.uc_words($controller, DS).’Controller.php’;
return $file;
}

//再看下validateControllerFileName()方法
public function validateControllerFileName($fileName)
{
if ($fileName && is_readable($fileName) && false===strpos($fileName, ‘//’)) {
return true;
}
return false;
}

[/php]

接着,通过getControllerClassName()方法获取完整类名;

[php]
$controllerClassName = $this->getControllerClassName($realModule, $controller);
if (!$controllerClassName) {
return false;
}

//这个方法可一点都没有Magento Style
public function getControllerClassName($realModule, $controller)
{
$class = $realModule.’_’.uc_words($controller).’Controller’;
return $class;
}
[/php]

最后,通过上面两个方法获取的结果,最后一次验证是否该类已经被定义,并存在于该类文件中。

[php]
// include controller file if needed
if (!$this->_includeControllerClass($controllerFileName, $controllerClassName)) {
return false;
}

/**
* 如果该控制器类还未定义,则include包含该控制器类的文件,并检测
* Include the file containing controller class if this class is not defined yet
*
* @param string $controllerFileName 控制器类文件名
* @param string $controllerClassName 控制器名
* @return bool
*/
protected function _includeControllerClass($controllerFileName, $controllerClassName)
{
//如果未检测到该控制器类被定义,则先包含类文件,然后检测。
if (!class_exists($controllerClassName, false)) {
//当然如果文件不存在,也不需要继续检测了
if (!file_exists($controllerFileName)) {
return false;
}
include $controllerFileName;

if (!class_exists($controllerClassName, false)) {
throw Mage::exception(‘Mage_Core’, Mage::helper(‘core’)->__(‘Controller file was loaded but class does not exist’));
}
}
return true;
}
[/php]

经过这三步验证之后,我们确定了该控制器类文件存在,并且该控制器类已经定义。If you’re ever wondered why controllers aren’t auto-loaded classes, wonder no more.

We’ll leave the specifics of all this as an exercise for the reader. The main take away here is that this method contains the name of the controller file and class that Magento will be looking for, and I’ve found this is the best place for a few temporary strategic var_dumps when debugging routing problems.

好了,跳出validateControllerClassName()方法,继续下面一段代码。如果没有获取到控制器类名的话,则循环下一个模块,继续查找。

[php]
if (!$controllerClassName) {
continue;
}
[/php]

当然,如果一切都没问题的话,则实例化该控制器类,通过如下代码,

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

最后,查看在该类中是否存在上面获取到的方法,通过如下代码,如果一切顺利的话将标志$found设置为true,跳出循环。

[php]
if (!$controllerInstance->hasAction($action)) {
continue;
}
$found = true;
break;
[/php]

路由分发的最后时刻

看到这里,加上第一篇,实际上一周时间已经结束了,但是很幸运,路由的最难的这一部分已经攻了下来。废话少说,看下面这段。在standard路由当中,这段代码是不会执行的,因为_noRouteShouldBeApplied()方法在该类中被硬编码返回false。这段代码会在admin路由中讲解。

[php]
/**
* if we did not found any siutibul
*/
if (!$found) {
if ($this->_noRouteShouldBeApplied()) {
$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;
}
}
[/php]

接下来,分发之前,给请求对象设置模块名(使用前端命名值),控制其名,方法名,以及最后真正匹配的$realModule。然后,从$p中获取其它传递的参数。最后的最后,进行路由分发操作。终于完成啦!!!

[php]
// 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;
[/php]

总结

You’ll find Magento routing as easy as pie!!!

转载请标明出处,Magento电子商务解决方案。

赞(0)
未经允许不得转载:锐想 » Magento路由分发过程解析(二):Standard路由对象
分享到: 更多 (0)

评论 10

评论前必须登录!

 

  1. #1

    请问三呢 🙁

    zeojon7年前 (2011-12-09)
    • 第三篇是关于admin,cms和noroute路由规则的。主要还是用到前端规则哇。所以就没怎么研究了。。

      ruiwant7年前 (2011-12-09)
      • 骚嘎~新换的界面漂亮诶 🙄

        zeojon7年前 (2011-12-13)
        • 嘿嘿,多多支持啦~~

          ruiwant7年前 (2011-12-13)
          • 你对分页的URL静态化有木有研究?

            zeojon7年前 (2011-12-14)
          • 这个目前还没深入研究,考虑这块直接做Ajax产品调用,对SEO有一定作用,不会产生太多动态页面。

            ruiwant7年前 (2011-12-16)
  2. #2

    很强大! :mrgreen:

    code7年前 (2011-12-17)
  3. #3

    博主好,想申请贵站友情链接!pr3 技术博客

    龙道名义7年前 (2012-02-29)
  4. #4

    楼主问你个问题,magento版本1.6进行模块开发,一切都按教程来做,在disable output modules可以看到自己的模块,可以在前台访问时出现404,我查了一下,好像config对象在读配置的时候都没有读到我开发的模块的信息。请问如何解决?

    chx7年前 (2012-09-18)

锐想电商 - 跨境电商经验交流分享

锐想无限