在ThinkPHP框架中,是支持URL路由功能,要啟用路由功能,需要設置ROUTER_ON 參數為true。
開啟路由功能后,系統會自動進行路由檢測,如果在路由定義里面找到和當前URL匹配的路由名稱,就會進行路由解析和重定向。
在tp中,程序會先從請求的url中解析出來一串字符,如果沒有開啟路由的話,那么tp就會從這串字符中解析出來模塊,控制器和方法以及參數。
如果開啟路由的話,那么tp會遍歷路由規則數組,然后用從url解析出來的這串字符依次和路由表達式進行正則匹配或者規則匹配,會優先匹配到一個路由表達式,找到該路由表達式對應的路由地址,從路由地址從解析出來控制器,方法以及參數。
系統在執行Dispatch解析時,會判斷當前URL是否存在定義的路由名稱,如果有就會按照定義的路由規則來進行URL解析。
// +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st <liu21st@gmail.com> // +---------------------------------------------------------------------- namespace Think; /** * ThinkPHP路由解析類 */ class Route { // 路由檢測 /* 路由檢測的基本思路: 1:首先獲得pathinfo的url值 2:先進行靜態路由的判斷,就是用處理后的url值和靜態路由規則進行比較,如果有相同的就說明匹配成功,返回true。 3:如果靜態路由匹配不成功,就進行動態路由匹配。動態路由分為正則路由和規則路由,先匹配正則路由,后匹配規則路由。 */ public static function check(){ //首先獲得配置文件中定義的pathinfo的分隔符。 $depr = C('URL_PATHINFO_DEPR'); //去掉pathinfo值的前后空格,如果有后綴的話(例如.html同樣去掉),得到比較純正的pathinfo值$regx $regx = preg_replace('/\.'.__EXT__.'$/i','',trim($_SERVER['PATH_INFO'],$depr)); // 分隔符替換 確保路由定義使用統一的分隔符 if('/' != $depr){ $regx = str_replace($depr,'/',$regx); } // URL映射定義(靜態路由) /* 如果靜態路由規則中有和這個url匹配的。那么得到路由地址,然后用parseurl對真正的url進行解析,解析出需要的變量返回。 關於parseurl的實現下面討論。 */ $maps = C('URL_MAP_RULES'); if(isset($maps[$regx])) { $var = self::parseUrl($maps[$regx]); $_GET = array_merge($var, $_GET); return true; } // 動態路由處理 /* 如果靜態路由沒有匹配成功,那么進入動態路由匹配。 首先從配置文件中獲得路由規則數組。 這個數組的中規則的寫法有兩種: 第一種是 '路由表達式'=>'路由地址和傳入參數' 第二種是 array('路由表達式','路由地址','傳入參數') 所以我們遍歷這個規則數組的時候需要區分對待。 通過判斷$rule變量是不是數字來判斷是第一種還是第二種。is_numeric($rule) 如果是第二種,那么規則數組的鍵應該是數字,當是第二種的時候,我們就從數組中出棧,取出 第一個元素即路由表達式賦值給$rule. 如果是第一種,那么什么都不用做,$rule自然就是路由表達式。 至此為止,我們的$rule變量里面存儲的就是路由表達式了。 */ $routes = C('URL_ROUTE_RULES'); if(!empty($routes)) { foreach ($routes as $rule=>$route){ if(is_numeric($rule)){ // 支持 array('rule','adddress',...) 定義路由 $rule = array_shift($route); } /* 下面還是對數組寫法的判斷。 接着上面的來,如果你的規則數組是數組格式array('路由表達式','路由地址','傳入參數') 經過上面的處理,第一個元素是規則表達式並且已經出棧。 那么現在route這個數組的第一個元素是url,第二個元素是參數,第三個元素是選項。 這里其實是實現文檔中所說的:當路由地址采用數組方式定義的時候,還可以傳入額外的路由參數。 我們得到第三個元素的值,通過這個可以進行url后綴檢測、請求類型檢測,最nb的是如果你這個參數 是一個函數,你還可以自定義函數檢測。 'blog/:id'=>array('blog/read','status=1&app_id=5',array('callback'=>'checkFun')), 就可以自定義定義checkFun函數來檢測是否生效,如果函數返回false則表示不生效。 */ if(is_array($route) && isset($route[2])){ // 路由參數 $options = $route[2]; if(isset($options['ext']) && __EXT__ != $options['ext']){ // URL后綴檢測 continue; } if(isset($options['method']) && REQUEST_METHOD != $options['method']){ // 請求類型檢測 continue; } // 自定義檢測 if(!empty($options['callback']) && is_callable($options['callback'])) { if(false === call_user_func($options['callback'])) { continue; } } } /* 上面的檢測完成后,下面進入正則路由。 判斷路由表達式是否是正則路由的條件是其第一個字符必須是/,並且$regx值必須符合這個路由表達式的正則規則 如果判斷是正則路由,那么就來處理對應的路由地址。 tp中的路由地址有多種形式,其中比較特殊的一種是函數形式。也就是說當我們的url匹配到一個路由表達式后, 就會去執行路由表達式對應的函數,而不是去解析平常的路由地址。tp在這里使用了php5.3的閉包。 首先會根據$route instanceof \Closure來判斷是不是一個閉包函數,如果是的話,就調用invokeRegx方法去 調用執行閉包函數並返回結果。 如果不是閉包函數,那么就是路由地址了。那么就調用parseRegex方法去解析這個路由地址,解析出控制器和方法以及參數等。 關於php的閉包:<a href="http://www.cnblogs.com/yjf512/archive/2012/10/29/2744702.html">http://www.cnblogs.com/yjf512/archive/2012/10/29/2744702.html</a>。 如果不是正則路由,那么就是規則路由了。 對於規則路由的判斷是如下邏輯: 首先計算出$regx值中的/的數量 然后計算出路由表達式中/的數量 如果是前者大於或者等於后者或者后者包含[ */ if(0===strpos($rule,'/') && preg_match($rule,$regx,$matches)) { // 正則路由 if($route instanceof \Closure) { // 執行閉包 $result = self::invokeRegx($route, $matches); // 如果返回布爾值 則繼續執行 return is_bool($result) ? $result : exit; }else{ return self::parseRegex($matches,$route,$regx); } }else{ // 規則路由 $len1 = substr_count($regx,'/'); $len2 = substr_count($rule,'/'); if($len1>=$len2 || strpos($rule,'[')) { if('$' == substr($rule,-1,1)) {// 完整匹配 if($len1 != $len2) { continue; }else{ $rule = substr($rule,0,-1); } } $match = self::checkUrlMatch($regx,$rule); if(false !== $match) { if($route instanceof \Closure) { // 執行閉包 $result = self::invokeRule($route, $match); // 如果返回布爾值 則繼續執行 return is_bool($result) ? $result : exit; }else{ return self::parseRule($rule,$route,$regx); } } } } } } return false; } // 檢測URL和規則路由是否匹配 /* 此函數主要用來檢測規則路由和url是否匹配。傳入的第一個參數是url,第二個參數是規則路由。 基本邏輯: 首先用explore函數把要檢測的url值和規則路由按照/進行拆分。 遍歷拆分后的規則路由數組,一一處理每個元素。 按照先特殊后普通的原則。 首先實現路由匹配的可選定義[:month\d]變量用[ ]包含起來后就表示該變量是路由匹配的可選變量。 如果要檢測的元素字符串以[:開頭,就截取出中括號中間的字符串賦值給$val,即:month\d. 下面是規則排除的實現邏輯。 如果$val是以:開頭的,就檢測$val字符串中|的位置 */ private static function checkUrlMatch($regx,$rule) { $m1 = explode('/',$regx); $m2 = explode('/',$rule); $var = array(); foreach ($m2 as $key=>$val){ if(0 === strpos($val,'[:')){ $val = substr($val,1,-1); } if(':' == substr($val,0,1)) {// 動態變量 if($pos = strpos($val,'|')){ // 使用函數過濾 $val = substr($val,1,$pos-1); } if(strpos($val,'\\')) { $type = substr($val,-1); if('d'==$type) { if(isset($m1[$key]) && !is_numeric($m1[$key])) return false; } $name = substr($val, 1, -2); }elseif($pos = strpos($val,'^')){ $array = explode('-',substr(strstr($val,'^'),1)); if(in_array($m1[$key],$array)) { return false; } $name = substr($val, 1, $pos - 1); }else{ $name = substr($val, 1); } $var[$name] = isset($m1[$key])?$m1[$key]:''; }elseif(0 !== strcasecmp($val,$m1[$key])){ return false; } } // 成功匹配后返回URL中的動態變量數組 return $var; } // 解析規范的路由地址 // 地址格式 [控制器/操作?]參數1=值1&參數2=值2... /* 該函數的主要功能是從url中解析出來控制器,操作和傳遞的參數。 首先檢測url中是否含有?,如果函數有的話,使用parse_url去處理url, 本函數解析一個 URL 並返回一個關聯數組,包含在 URL 中出現的各種組成部分。 本函數不是用來驗證給定 URL 的合法性的,只是將其分解為下面列出的部分。不完整的 URL 也被接受。 參考:http://www.php100.com/cover/php/1764.html. $path將得到域名后面,?之前的字符串。使用/分割$path. $info[query]得到的是?后面的get參數字符串。我們這里使用parse_url函數可以把這個get參數字符串 分割成一個數組。 */ private static function parseUrl($url) { $var = array(); if(false !== strpos($url,'?')) { // [控制器/操作?]參數1=值1&參數2=值2... $info = parse_url($url); $path = explode('/',$info['path']); parse_str($info['query'],$var); }elseif(strpos($url,'/')){ // [控制器/操作] $path = explode('/',$url); }else{ // 參數1=值1&參數2=值2... parse_str($url,$var); } //在上面處理后,檢測如果$path存在,就返回$path數組的最后一個元素為操作方法。 //再次檢測$path變量,如果還存在,就再次返回最后一個元素作為控制器。 //再次檢測$path變量,如果還存在,就再次返回作為模塊名稱。 //例如$path=array('home','index','init');那么按照下面的操作,action是init,控制器是index。模塊就是home //分別賦值給$var數組並返回$var.這樣我么就從url中解析處理模塊,控制器,操作以及其他get參數。 if(isset($path)) { $var[C('VAR_ACTION')] = array_pop($path); if(!empty($path)) { $var[C('VAR_CONTROLLER')] = array_pop($path); } if(!empty($path)) { $var[C('VAR_MODULE')] = array_pop($path); } } return $var; } // 解析規則路由 // '路由規則'=>'[控制器/操作]?額外參數1=值1&額外參數2=值2...' // '路由規則'=>array('[控制器/操作]','額外參數1=值1&額外參數2=值2...') // '路由規則'=>'外部地址' // '路由規則'=>array('外部地址','重定向代碼') // 路由規則中 :開頭 表示動態變量 // 外部地址中可以用動態變量 采用 :1 :2 的方式 // 'news/:month/:day/:id'=>array('News/read?cate=1','status=1'), // 'new/:id'=>array('/new.php?id=:1',301), 重定向 private static function parseRule($rule,$route,$regx) { // 獲取路由地址規則 $url = is_array($route)?$route[0]:$route; // 獲取URL地址中的參數 $paths = explode('/',$regx); // 解析路由規則 $matches = array(); $rule = explode('/',$rule); foreach ($rule as $item){ $fun = ''; if(0 === strpos($item,'[:')){ $item = substr($item,1,-1); } if(0===strpos($item,':')) { // 動態變量獲取 if($pos = strpos($item,'|')){ // 支持函數過濾 $fun = substr($item,$pos+1); $item = substr($item,0,$pos); } if($pos = strpos($item,'^') ) { $var = substr($item,1,$pos-1); }elseif(strpos($item,'\\')){ $var = substr($item,1,-2); }else{ $var = substr($item,1); } $matches[$var] = !empty($fun)? $fun(array_shift($paths)) : array_shift($paths); }else{ // 過濾URL中的靜態變量 array_shift($paths); } } if(0=== strpos($url,'/') || 0===strpos($url,'http')) { // 路由重定向跳轉 if(strpos($url,':')) { // 傳遞動態參數 $values = array_values($matches); $url = preg_replace_callback('/:(\d+)/', function($match) use($values){ return $values[$match[1] - 1]; }, $url); } header("Location: $url", true,(is_array($route) && isset($route[1]))?$route[1]:301); exit; }else{ // 解析路由地址 $var = self::parseUrl($url); // 解析路由地址里面的動態參數 $values = array_values($matches); foreach ($var as $key=>$val){ if(0===strpos($val,':')) { $var[$key] = $values[substr($val,1)-1]; } } $var = array_merge($matches,$var); // 解析剩余的URL參數 if(!empty($paths)) { preg_replace_callback('/(\w+)\/([^\/]+)/', function($match) use(&$var){ $var[strtolower($match[1])]=strip_tags($match[2]);}, implode('/',$paths)); } // 解析路由自動傳入參數 if(is_array($route) && isset($route[1])) { if(is_array($route[1])){ $params = $route[1]; }else{ parse_str($route[1],$params); } $var = array_merge($var,$params); } $_GET = array_merge($var,$_GET); } return true; } // 解析正則路由 // '路由正則'=>'[控制器/操作]?參數1=值1&參數2=值2...' // '路由正則'=>array('[控制器/操作]?參數1=值1&參數2=值2...','額外參數1=值1&額外參數2=值2...') // '路由正則'=>'外部地址' // '路由正則'=>array('外部地址','重定向代碼') // 參數值和外部地址中可以用動態變量 采用 :1 :2 的方式 // '/new\/(\d+)\/(\d+)/'=>array('News/read?id=:1&page=:2&cate=1','status=1'), // '/new\/(\d+)/'=>array('/new.php?id=:1&page=:2&status=1','301'), 重定向 private static function parseRegex($matches,$route,$regx) { // 獲取路由地址規則 $url = is_array($route)?$route[0]:$route; $url = preg_replace_callback('/:(\d+)/', function($match) use($matches){return $matches[$match[1]];}, $url); if(0=== strpos($url,'/') || 0===strpos($url,'http')) { // 路由重定向跳轉 header("Location: $url", true,(is_array($route) && isset($route[1]))?$route[1]:301); exit; }else{ // 解析路由地址 $var = self::parseUrl($url); // 處理函數 foreach($var as $key=>$val){ if(strpos($val,'|')){ list($val,$fun) = explode('|',$val); $var[$key] = $fun($val); } } // 解析剩余的URL參數 $regx = substr_replace($regx,'',0,strlen($matches[0])); if($regx) { preg_replace_callback('/(\w+)\/([^\/]+)/', function($match) use(&$var){ $var[strtolower($match[1])] = strip_tags($match[2]); }, $regx); } // 解析路由自動傳入參數 if(is_array($route) && isset($route[1])) { if(is_array($route[1])){ $params = $route[1]; }else{ parse_str($route[1],$params); } $var = array_merge($var,$params); } $_GET = array_merge($var,$_GET); } return true; } // 執行正則匹配下的閉包方法 支持參數調用 static private function invokeRegx($closure, $var = array()) { $reflect = new \ReflectionFunction($closure); $params = $reflect->getParameters(); $args = array(); array_shift($var); foreach ($params as $param){ if(!empty($var)) { $args[] = array_shift($var); }elseif($param->isDefaultValueAvailable()){ $args[] = $param->getDefaultValue(); } } return $reflect->invokeArgs($args); } // 執行規則匹配下的閉包方法 支持參數調用 static private function invokeRule($closure, $var = array()) { $reflect = new \ReflectionFunction($closure); $params = $reflect->getParameters(); $args = array(); foreach ($params as $param){ $name = $param->getName(); if(isset($var[$name])) { $args[] = $var[$name]; }elseif($param->isDefaultValueAvailable()){ $args[] = $param->getDefaultValue(); } } return $reflect->invokeArgs($args); } }
