Yii源碼閱讀筆記 - 應用模塊化

2014-11-20 四php

By youngsterxyf

概述

Yii框架有個「模塊(Module)」的概念,與「應用(Application)」相似,模塊必須歸屬於一個父模塊或者一個應用,模塊不能單獨部署,一個應用不必定要分模塊。html

由此能夠看到,Yii的「模塊」和「應用」相似於Django框架中的「應用(App)」和「項目(Project)」。git

當一個應用的規模大到必定的程度 - 可能涉及多個團隊來開發,就應該考慮分「模塊」開發。「模塊」一般對應應用的一個相對獨立的功能。github

一個模塊化的Yii框架應用的工程目錄結構大體示例以下:express

Yii-WebApp-Modules

上圖所示項目有一個名爲「forum」的模塊,該模塊下也有本身的componentscontrollersmodelsviewsextensions目錄,與一個普通的/不分模塊的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;  }  } } 

從上述代碼能夠看到,每一個模塊對象也會記錄它的歸屬 - 屬於應用對象,仍是某個父模塊對象。

自定義模塊類無需定義本身的構造方法,構造方法能夠間接繼承自抽象類CModuleCWebModule類並未定義本身的構造方法),其構造方法實現以下:

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')  )  ) ) 

多個keyname能夠相同,若是相同,則會合並數組。如:

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'),  ),  ), ); 

擦,牛逼到死啊!

相關文章
相關標籤/搜索