標籤:
2014-11-12 三
By youngsterxyf概述
Yii架構的路由解析功能由核心組件urlManager來完成。路由的形式有兩種:
- get:通過URL中查詢字串(query string)參數r來指定路由,如:
r=controllerID/actionID
- path:直接通過URL來指定,如:
/controllerID/actionID
預設使用get路由形式。由於Yii中controller類命名和action方法都是按照規則命名的,而路由也是按照規則來匹配的,所以完全可以不用額外配置urlManager。
若需要使用path方式,則可如下配置:
"components" => array( ‘urlManager‘ => array( ‘urlFormat‘ => ‘path‘, ‘rules‘ => array( ... ),),
進一步說明可參考RESTful API設計的一點經驗一文。
分析
在“請求處理基本流程”一篇可以看到Yii架構路由解析流程的入口在類CWebApplication的processRequest方法中:
$route=$this->getUrlManager()->parseUrl($this->getRequest());
其中getUrlManager方法定義於類CApplication中,作用是初始化擷取URL管理組件(ID為urlManager),實現如下:
public function getUrlManager(){ return $this->getComponent(‘urlManager‘);}
在擷取urlManager組件對象過程中,會對對象做初始化,調用對象的init方法,見類CUrlManager的init方法實現:
public function init(){ parent::init(); $this->processRules();}
其中調用的方法processRules,是根據配置的rules解析建立規則對象,放到屬性_rules中,實現如下:
protected function processRules(){ // 如果未配置rules,或使用的路由形式是get,則根本無需解析路由規則 if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) return; // 否則嘗試從緩衝中讀取解析好的路由規則 if($this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) { $hash=md5(serialize($this->rules)); if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) { $this->_rules=$data[0]; return; } } // 否則逐條路由規則解析 foreach($this->rules as $pattern=>$route) $this->_rules[]=$this->createUrlRule($route,$pattern); // 嘗試緩衝解析好的路由規則 if(isset($cache)) $cache->set(self::CACHE_KEY,array($this->_rules,$hash));}
從上述代碼中,在解析建立規則對象前會先檢查是否已緩衝瞭解析建立好的規則,如果沒有,則在解析建立好規則後,將這些規則緩衝起來。這樣就避免了每次請求處理都要解析一次rules列表。 但這裡需要注意的是urlManager組件預設使用ID為cache的緩衝組件(CUrlManager類的屬性cacheID預設值為cache),而核心組件並不包含ID為cache的緩衝組件,所以若希望緩衝解析好路由規則, 則應該配置ID為cache的緩衝組件,如果緩衝組件的ID不是cache,則需要配置urlManager組件的cacheID屬性。
如果沒有設定緩衝組件,或者緩衝中未找到解析好的路由規則,則需要對配置的rules逐條解析,解析過程見類CUrlManager的createUrlRule方法實現:
/** * Creates a URL rule instance. * The default implementation returns a CUrlRule object. * @param mixed $route the route part of the rule. This could be a string or an array * @param string $pattern the pattern part of the rule * @return CUrlRule the URL rule instance * @since 1.1.0 */protected function createUrlRule($route,$pattern){ // 說明可以配置自訂的路由規則解析類 if(is_array($route) && isset($route[‘class‘])) return $route; else { $urlRuleClass=Yii::import($this->urlRuleClass,true); return new $urlRuleClass($route,$pattern); }}
以以下rules配置為例:
‘rules‘ => array( array(‘industry/index‘, ‘pattern‘ => ‘/v1/partner/industry/‘, ‘verb‘ => ‘GET‘), array(‘token/create‘, ‘pattern‘ => ‘/v1/partner/token‘, ‘verb‘ => ‘POST‘),)
在處理第一條規則時,方法createUrlRule的參數$pattern的值為數組的索引0,$route的值為關聯陣列array(‘industry/index‘, ‘pattern‘ => ‘/v1/partner/industry/‘, ‘verb‘ => ‘GET‘), 但因為$route裡沒有設定class欄位,所以走的是else分支 - 先引入類CUrlRule($this->urlRuleClass的預設值),然後根據$route、$pattern執行個體化類CUrlRule,該類也定義在檔案yii/framework/web/CUrlManager.php中, 直接繼承自抽象類別CBaseUrlRule。CUrlRule的構造方法實現如下:
public function __construct($route,$pattern){ if(is_array($route)) { // 從這裡可知$route支援‘urlSuffix‘, ‘caseSensitive‘, ‘defaultParams‘, ‘matchValue‘, ‘verb‘, ‘parsingOnly‘這些配置項 foreach(array(‘urlSuffix‘, ‘caseSensitive‘, ‘defaultParams‘, ‘matchValue‘, ‘verb‘, ‘parsingOnly‘) as $name) { if(isset($route[$name])) $this->$name=$route[$name]; } // 如果$route中有pattern配置項,則將配置值賦值給$pattern if(isset($route[‘pattern‘])) $pattern=$route[‘pattern‘]; // 而$route的第一個配置項才是真正的目標路由 $route=$route[0]; } $this->route=trim($route,‘/‘); $tr2[‘/‘]=$tr[‘/‘]=‘\\/‘; if(strpos($route,‘<‘)!==false && preg_match_all(‘/<(\w+)>/‘,$route,$matches2)) { foreach($matches2[1] as $name) $this->references[$name]="<$name>"; } // 是否帶協議頭 $this->hasHostInfo=!strncasecmp($pattern,‘http://‘,7) || !strncasecmp($pattern,‘https://‘,8); // 如果原$route有verb配置項 // verb配置支援多個HTTP方法,以空格或逗號分隔,如:“GET,POST” if($this->verb!==null) $this->verb=preg_split(‘/[\s,]+/‘,strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY); // $pattern中類正則片段支援兩種形式:命名的和未命名的,如“<id:\d+>”和“<\d+>” if(preg_match_all(‘/<(\w+):?(.*?)?>/‘,$pattern,$matches)) { $tokens=array_combine($matches[1],$matches[2]); foreach($tokens as $name=>$value) { if($value===‘‘) $value=‘[^\/]+‘; $tr["<$name>"]="(?P<$name>$value)"; if(isset($this->references[$name])) $tr2["<$name>"]=$tr["<$name>"]; else $this->params[$name]=$value; } } // 好吧,之後的這段代碼我還沒太看懂作用 // 就是為了將$pattern轉換成一個真正的Regex? $p=rtrim($pattern,‘*‘); $this->append=$p!==$pattern; $p=trim($p,‘/‘); $this->template=preg_replace(‘/<(\w+):?.*?>/‘,‘<$1>‘,$p); $this->pattern=‘/^‘.strtr($this->template,$tr).‘\/‘; if($this->append) $this->pattern.=‘/u‘; else $this->pattern.=‘$/u‘; if($this->references!==array()) $this->routePattern=‘/^‘.strtr($this->route,$tr2).‘$/u‘; if(YII_DEBUG && @preg_match($this->pattern,‘test‘)===false) throw new CException(Yii::t(‘yii‘,‘The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.‘, array(‘{route}‘=>$route,‘{pattern}‘=>$pattern)));}
在得到urlManager組件對象後,調用其parseUrl方法,實現如下:
public function parseUrl($request){ if($this->getUrlFormat()===self::PATH_FORMAT) { $rawPathInfo=$request->getPathInfo(); $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix); foreach($this->_rules as $i=>$rule) { if(is_array($rule)) $this->_rules[$i]=$rule=Yii::createComponent($rule); // 逐個路由規則匹配 if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false) // 即使匹配到了路由規則,也還是得看一下URL中是否指定了路由,是的話則優先使用URL中指定的路由 return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; } // 如果一定要匹配到某個路由規則才行,那麼執行到這裡就表示未有匹配的路由規則,所以就拋404錯誤了。 if($this->useStrictParsing) throw new CHttpException(404,Yii::t(‘yii‘,‘Unable to resolve the request "{route}".‘, array(‘{route}‘=>$pathInfo))); // 否則先返回請求路徑作為目標路由 else return $pathInfo; } // 如果使用的是get路由形式,則從GET請求的查詢字串或POST請求的請求體找目標路由 elseif(isset($_GET[$this->routeVar])) return $_GET[$this->routeVar]; elseif(isset($_POST[$this->routeVar])) return $_POST[$this->routeVar]; else return ‘‘;}
方法的參數是一個request組件對象。
先判斷應用使用的路由形式是否為path,如果不是,則根據路由的參數名(預設為r,由於urlManager類的routeVar屬性是public的,所以可以通過配置routeVar的值來修改路由參數名)擷取路由。並且路由可以通過GET方法放在URL查詢字串中,也可以通過POST方法放在請求體中。
對於path形式的路由,解析過程則要複雜一些。先通過request組件對象的getPathInfo方法取到請求的URL(會對原本的請求URL做一定的處理),然後根據解析好的路由規則列表逐個匹配。其中CUrlRule類的parseUrl方法實現如下:
public function parseUrl($manager,$request,$pathInfo,$rawPathInfo){ // 先檢查HTTP謂詞(verb)是否匹配 if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true)) return false; // 是否關心大小寫 if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) $case=‘‘; else $case=‘i‘; // urlSiffix配置項是用來幹嘛的? if($this->urlSuffix!==null) $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix); // URL suffix required, but not found in the requested URL if($manager->useStrictParsing && $pathInfo===$rawPathInfo) { $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; if($urlSuffix!=‘‘ && $urlSuffix!==‘/‘) return false; } if($this->hasHostInfo) $pathInfo=strtolower($request->getHostInfo()).rtrim(‘/‘.$pathInfo,‘/‘); $pathInfo.=‘/‘; // 正則匹配:用pattern來匹配路徑 if(preg_match($this->pattern.$case,$pathInfo,$matches)) { // 可以配置defaultParams數組來為請求未提供的必要參數指定預設值 foreach($this->defaultParams as $name=>$value) { if(!isset($_GET[$name])) $_REQUEST[$name]=$_GET[$name]=$value; } $tr=array(); foreach($matches as $key=>$value) { if(isset($this->references[$key])) $tr[$this->references[$key]]=$value; elseif(isset($this->params[$key])) $_REQUEST[$key]=$_GET[$key]=$value; } if($pathInfo!==$matches[0]) // there‘re additional GET params $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),‘/‘)); if($this->routePattern!==null) return strtr($this->route,$tr); else return $this->route; } else return false;}
從上述代碼可以看出,路由解析關鍵是根據$pattern匹配請求URL,並從URL取出需要的東西作為請求參數,一旦匹配,就以$route作為該次請求的目標路由。
獲得目標路由後,就可以根據目標路由尋找調用對應的controller和action了。
Yii源碼閱讀筆記 - 路由解析