概述
Yii框架有个“模块(Module)”的概念,与“应用(Application)”相似,模块必须归属于一个父模块或者一个应用,模块不能单独部署,一个应用不必定要分模块。html
由此能够看到,Yii的“模块”和“应用”相似于Django框架中的“应用(App)”和“项目(Project)”。git
当一个应用的规模大到必定的程度 - 可能涉及多个团队来开发,就应该考虑分“模块”开发。“模块”一般对应应用的一个相对独立的功能。github
一个模块化的Yii框架应用的工程目录结构大体示例以下:express
上图所示项目有一个名为“forum”的模块,该模块下也有本身的components
、controllers
、models
、views
、extensions
目录,与一个普通的/不分模块的Yii框架Web应用的项目结构很是类似。数组
Yii框架模块化应用的全部模块默认都是放在protected/modules
目录下,每一个模块的内容又各自放在以模块ID(如forum
)为名称的子目录下,而且在模块子目录下要有一个模块类文件,如ForumModule.php
,该类文件的命名规范是:模块ID首字母大写,而后拼接上字符串Module。app
模块化的应用须要在配置文件中配置modules
一项 - 指定模块列表,示例以下:框架
'modules' => array(
'forum' => array( ... ), 'anotherModule', ... ),
每一个模块的配置,能够只指定模块ID,也能够经过数组来指定额外的信息,如模块类、类实例化参数、params、components,以及子模块等等。Yii中模块是能够嵌套的,而且嵌套深度没有限制(有这个必要么?不要玩脱了啊)。yii
对应某个模块中的控制器及控制器中的Action,路由中须要带模块ID前缀,如moduleID/controllerID/actionID
,对于嵌套的模块,路由的形式则为parentModuleID/childModuleID/controllerID/actionID
。路由分发逻辑会根据模块ID到配置信息中查找对应的模块,最终分发到某个模块的某个控制器的某个Action中作处理。ide
另外,Yii框架应用的模块化并非必须把全部功能逻辑都拆分到各个模块,而是能够部分功能逻辑归到应用,部分逻辑归到模块,便可以不完全地模块化,但我的认为最好别这么玩(应用下的controller的id和模块的id冲突怎么办?),而且最好不要用模块嵌套,以避免搞得过于复杂,下降项目的可维护性。
分析
先从继承关系上看看“模块”与“应用”的类似性:
CWebApplication
->CApplication
->CModule
->CComponent
- 自定义模块类 ->
CWebModule
->CModule
->CComponent
由此能够看到继承链中类CModule
及上溯类的属性和方法,“模块”类和“应用”都有。
由Yii源码阅读笔记 - 请求处理基本流程一文可知,应用配置的加载是抽象类CApplication的构造方法中调用方法configure
来完成的, 该方法定义于类CModule
中,实现以下:
/**
* Configures the module with the specified configuration. * @param array $config the configuration array */ public function configure($config) { if(is_array($config)) { foreach($config as $key=>$value) $this->$key=$value; } }
对于配置项“modules”的加载,则是经过类CComponent
中的魔术方法__set
最终调用类CModule
中的setModules
方法来完成的:
/**
* Configures the sub-modules of this module. * * Call this method to declare sub-modules and configure them with their initial property values. * The parameter should be an array of module configurations. Each array element represents a single module, * which can be either a string representing the module ID or an ID-configuration pair representing * a module with the specified ID and the initial property values. * * For example, the following array declares two modules: * <pre> * array( * 'admin', // a single module ID * 'payment'=>array( // ID-configuration pair * 'server'=>'paymentserver.com', * ), * ) * </pre> * * By default, the module class is determined using the expression <code>ucfirst($moduleID).'Module'</code>. * And the class file is located under <code>modules/$moduleID</code>. * You may override this default by explicitly specifying the 'class' option in the configuration. * * You may also enable or disable a module by specifying the 'enabled' option in the configuration. * * @param array $modules module configurations. */ public function setModules($modules) { foreach($modules as $id=>$module) { // 若是只指定了模块的id if(is_int($id)) { $id=$module; $module=array(); } // 若是未指定模块对应的模块类,则默认经过路径别名$id.'.'.ucfirst($id).'Module'来查找对应的模块类 if(!isset($module['class'])) { Yii::setPathOfAlias($id,$this->getModulePath().DIRECTORY_SEPARATOR.$id); $module['class']=$id.'.'.ucfirst($id).'Module'; } // 将模块配置信息存入属性_moduleConfig中 if(isset($this->_moduleConfig[$id])) $this->_moduleConfig[$id]=CMap::mergeArray($this->_moduleConfig[$id],$module); else $this->_moduleConfig[$id]=$module; } }
能够看到模块列表配置信息加载后并未对模块类进行实例化初始化。
请求处理在路由解析获得目标路由后,调用方法createController
来作路由分发(这样表述可能不太严谨),该方法定义于类CWebApplication
中,实现以下所示:
public function createController($route,$owner=null)
{ // 若是未提供参数$owner,即未指定当前$route所属的模块,则默认当前应用对象为owner,能够将应用当作是顶级模块 if($owner===null) $owner=$this; // 若是路由为空,则使用默认路由 // 应用的默认路由ID是site,模块的默认路由ID为default if(($route=trim($route,'/'))==='') $route=$owner->defaultController; // 路由是否大小写敏感 $caseSensitive=$this->getUrlManager()->caseSensitive; $route.='/'; // 若是路由中还有斜杠 // 注意这里是个while循环 while(($pos=strpos($route,'/'))!==false) { // 取出第一个斜杠以前的部分,用于以后的代码看看是否有对应该ID的controller或module $id=substr($route,0,$pos); if(!preg_match('/^\w+$/',$id)) return null; if(!$caseSensitive) $id=strtolower($id); // 取出第一个斜杠以后的部分,用于可能的下一次循环处理 $route=(string)substr($route,$pos+1); // 看看是不是第一次循环处理 // $basePath是在第一次循环处理时在这个if条件分支中才赋值的,因此第一次循环处理到这里时$basePath是未定义 if(!isset($basePath)) // first segment { // 先从应用或模块配置的controllerMap中看看是否有$id为key的controller,如有,则直接实例化对应的controll类并返回 if(isset($owner->controllerMap[$id])) { return array( Yii::createComponent($owner->controllerMap[$id],$id,$owner===$this?null:$owner), $this->parseActionParams($route), ); } // 看看当前应用的modules配置项中是否有以$id为key的模块,或当前模块的modules配置中是否有以$id为key的子模块,若是有则以$module为$owner参数值递归调用createController方法 if(($module=$owner->getModule($id))!==null) return $this->createController($route,$module); // 当前应用或模块下的控制器类的存放目录 $basePath=$owner->getControllerPath(); $controllerID=''; } else $controllerID.='/'; // 默认以$id为controller的ID,在当前应用或模块下查找是否有对应的控制器类文件 $className=ucfirst($id).'Controller'; $classFile=$basePath.DIRECTORY_SEPARATOR.$className.'.php'; // 擦,怎么多出一个命名空间的东西? if($owner->controllerNamespace!==null) $className=$owner->controllerNamespace.'\\'.$className; // 若是有对应的控制器类文件,则尝试加载实例化 if(is_file($classFile)) { if(!class_exists($className,false)) require($classFile); if(class_exists($className,false) && is_subclass_of($className,'CController')) { $id[0]=strtolower($id[0]); return array( new $className($controllerID.$id,$owner===$this?null:$owner), $this->parseActionParams($route), ); } return null; } // 不然把$id当作普通的一级目录名 $controllerID.=$id; $basePath.=DIRECTORY_SEPARATOR.$id; } }
从上述代码中能够看到,控制器类在实例化时须要传入该控制器类属于应用仍是属于某个模块,这个归属记录在控制器类实例的_module属性中,若是属性值为null,则表示属于应用,_module属性定义于类CController
中。
咱们来看看上述代码中调用的方法getModule
的实现,这个方法调用的$owner
多是应用对象也多是某个模块类对象,该方法定义于抽象类CModule
中,实现以下:
public function getModule($id)
{ // 若是$id对应的module已经实例化好,则直接返回 if(isset($this->_modules[$id]) || array_key_exists($id,$this->_modules)) return $this->_modules[$id]; // 看是否配置了$id对应的module elseif(isset($this->_moduleConfig[$id])) { $config=$this->_moduleConfig[$id]; if(!isset($config['enabled']) || $config['enabled']) { Yii::trace("Loading \"$id\" module",'system.base.CModule'); $class=$config['class']; unset($config['class'], $config['enabled']); // 实例化module,module的$owner多是当前应用对象,也多是一个模块对象 if($this===Yii::app()) $module=Yii::createComponent($class,$id,null,$config); else $module=Yii::createComponent($class,$this->getId().'/'.$id,$this,$config); return $this->_modules[$id]=$module; } } }
从上述代码能够看到,每一个模块对象也会记录它的归属 - 属于应用对象,仍是某个父模块对象。
自定义模块类无需定义本身的构造方法,构造方法能够间接继承自抽象类CModule
(CWebModule
类并未定义本身的构造方法),其构造方法实现以下:
public function __construct($id,$parent,$config=null)
{ $this->_id=$id; $this->_parentModule=$parent; // set basePath at early as possible to avoid trouble if(is_string($config)) $config=require($config); if(isset($config['basePath'])) { $this->setBasePath($config['basePath']); unset($config['basePath']); } Yii::setPathOfAlias($id,$this->getBasePath()); $this->preinit(); $this->configure($config); $this->attachBehaviors($this->behaviors); $this->preloadComponents(); $this->init(); }
这个方法与Web应用类的构造方法(定义于抽象类CApplication
中)实现很是类似。这两个构造方法是调用同一个configure
方法来加载配置的,因此不少“应用”的配置项,“模块”也都支持。 从上述模块的构造方法中能够看到当前模块属于哪一个父模块是记录在属性_parentModule
中的,若是该属性值为null,则表示当前模块属于当前Web应用对象。这样经过获取控制器对象的_module
属性值,继而获取模块对象的_parentModule
属性值,就能知道整个归属关系链。
注:如下部分是对Yii源码阅读笔记 - 路由解析一文的补充。
前面讨论的方法createController
中还调用了方法parseActionParams
来解析获取Action的ID,也定义于类CWebApplication
中,实现以下:
/**
* Parses a path info into an action ID and GET variables. * @param string $pathInfo path info * @return string action ID */ protected function parseActionParams($pathInfo) { // 屌!其实就是以斜杠分割$pathInfo取第一个部分做为Action的ID if(($pos=strpos($pathInfo,'/'))!==false) { $manager=$this->getUrlManager(); // 第一个部分以外剩余的部分作请求参数解析 $manager->parsePathInfo((string)substr($pathInfo,$pos+1)); $actionID=substr($pathInfo,0,$pos); return $manager->caseSensitive ? $actionID : strtolower($actionID); } else // 若是$pathInfoH中不存在斜杠,则就将$pathInfo做为Action的ID return $pathInfo; }
其中调用的parsePathInfo
方法,定义于类CUrlManager
中,实现以下:
/**
* Parses a path info into URL segments and saves them to $_GET and $_REQUEST. * @param string $pathInfo path info */ public function parsePathInfo($pathInfo) { if($pathInfo==='') return; $segs=explode('/',$pathInfo.'/'); $n=count($segs); for($i=0;$i<$n-1;$i+=2) { $key=$segs[$i]; if($key==='') continue; $value=$segs[$i+1]; if(($pos=strpos($key,'['))!==false && ($m=preg_match_all('/\[(.*?)\]/',$key,$matches))>0) { $name=substr($key,0,$pos); for($j=$m-1;$j>=0;--$j) { if($matches[1][$j]==='') $value=array($value); else $value=array($matches[1][$j]=>$value); } if(isset($_GET[$name]) && is_array($_GET[$name])) $value=CMap::mergeArray($_GET[$name],$value); $_REQUEST[$name]=$_GET[$name]=$value; } else $_REQUEST[$key]=$_GET[$key]=$value; } }
仔细看看上述代码的逻辑吧,累觉不爱啊!
这个方法的做用:在目标路由去除Controller ID和Action ID两个部分后,从剩余部分中按必定规则解析出请求参数,那么规则是什么样的呢?
举例来讲,这个目标路由剩余部分的基本形式以下所示:
key/value/key/value/
其中key
为参数名,value
为参数值。
但key
的形式能够数组取值的形式,如:
name[x][y][z]
这种形式的key
对应的value
会从原来的字符串转换成数组形式,如:
array(
'x' => array( 'y' => array( 'z' => array('value') ) ) )
多个key
的name
能够相同,若是相同,则会合并数组。如:
name[a][b][c]/value1/name[A][B][C]/value2/name[x][y][z]/value3/name[a][X][f]/value4/
最终会转换成请求参数项:
$_REQUEST['name'] = $_GET['name'] = array(
'a' => array( 'b' => array( 'c' => array('value1'), ), 'X' => array( 'f' => array('value4'), ), ), 'A' => array( 'B' => array( 'C' => array('value2'), ), ), 'x' => array( 'y' => array( 'z' => array('value3'), ), ), );
擦,牛逼到死啊!