Thinkphp源碼分析系列(六)–路由機制


在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);
 }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM