TP的路由

这里是用 thinkphp 3.2.0 版本的框架来做分析的

TP 的路由支持的还是蛮多的,这里将解析 TP 路由的源码。

一些准备

在准备开始处理 PATH_INFO 的时候,先对其做了一些过滤。

<?php
$depr   =   C('URL_PATHINFO_DEPR');
$regx   =   preg_replace('/\.'.__EXT__.'$/i','',trim($_SERVER['PATH_INFO'],$depr));

去除了后面的 URL_PATHINFO_DEPR,去除了结尾的 .php (当然结尾是看定义的)。

<?php
if('/' != $depr){
    $regx = str_replace($depr,'/',$regx);
}

为了保证分隔符的统一,之后又进行了一次替换。

静态路由

首先系统会先解析静态路由,而静态路由是用户自己配置的。

关于路由的定义,可以看一下官方文档 路由定义

<?php
$maps   =   C('URL_MAP_RULES');
if(isset($maps[$regx])) {
    $var    =   self::parseUrl($maps[$regx]);
    $_GET   =   array_merge($var, $_GET);
    return true;                
} 

其实通过这段代码很容易分析出,所谓的静态路由其实就是一些路由的映射,不包含动态参数。适用的场景很少。

经历了 parseUrl 之后,获取到了相关参数,并合并到 $_GET 中。

这里值得注意的就是,$_GET 是第二个参数,也就是说,$var 存在被覆盖的可能性。(这是一个隐性的坑,也表明,这个静态配置可以动态修改)

到这里,静态路由解析已经结束。

动态路由

如果没有配置,直接结束。

如果配置了,则会对路由规则进行遍历。

两种配置方式

<?php
if(is_numeric($rule)){
    // 支持 array('rule','adddress',...) 定义路由
    $rule   =   array_shift($route);
}

可以说,TP 的兼容性真的很强,支持多种写法,虽然我不建议。

上面的代码,为下面两种写法做了铺垫:

<?php
// 第一种写法
[
    'rule' => ['address', 'options']
]

// 第二种写法
[
    ['rule', 'address', 'options']
]

一些过滤

如果 $route 是数组,并且有第二个参数,那么会进行一些列过滤。

<?php
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;
        }
    }                    
}
  • 后缀过滤
  • 请求方式过滤(很棒的一个东西)
  • 回调函数的过滤

正则匹配

<?php
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);
    }
}

如果 $rule 是以 / 开头,并且正则匹配路由能够通过,那么,就按照正则路由处理。

如果正则是以 # 开头的就不行了,这点需要注意。

如果 $route 是一个函数,则执行函数。(关于 invokeRegx,要手动在 匿名函数 中给 $_GET 赋值我们匹配到的路由参数)

如果 $route 不是函数,则直接当做正则来解析。详细的 parseRegex 解析看最后的 parseRegex 部分。

规则路由

<?php
$len1   =   substr_count($regx,'/');
$len2   =   substr_count($rule,'/');
if($len1>=$len2 || strpos($rule,'[')) {
    ...
}

这里的其实也是正则解析,但是有一点不同的是,这里只有出现可选路由的时候或者请求路由的层级大于等于规则的层级才能够被匹配。

全匹配

<?php
if('$' == substr($rule,-1,1)) {// 完整匹配
    if($len1 != $len2) {
        continue;
    }else{
        $rule =  substr($rule,0,-1);
    }
}

如果 $rule 是以 $ 结尾的,那么表示 请求路由 的层级需要和 规则 的层级保持一致才可。

接下来会解析规则,使用的函数是 checkUrlMatch

获得匹配的数据之后,和之前的处理方式差不多,如果 $rule 是函数,则执行,不是则匹配。

部分源码解析

parseUrl 解析

这里的 $url 参数支持三种格式:

  • path + query (/news/index?type=top)
  • path (news/index)
  • query (m=news&a=top)

解析的顺序,从上到下。

path + query

格式:/news/index?type=top

<?php
if(false !== strpos($url,'?')) { // [控制器/操作?]参数1=值1&参数2=值2...
    $info   =  parse_url($url);
    $path   = explode('/',$info['path']);
    parse_str($info['query'],$var);
}

通过判断是否有 ? 辨别是否属于第一种情况。

通过 parse_url 解析 path 得到如下结构:

Array
(
    [path] => /news/index
    [query] => type=top
)

接下来可以分别解析 pathquery,并通过 parse_str 将解析的 query 存入 $var,从而向后流转。

path

格式:news/index

<?php
if(strpos($url,'/')){ // [控制器/操作]
    $path = explode('/',$url);
}

这个就简单了,如果存在 / 就直接解析 path

query

格式:m=news&a=top

<?php
parse_str($url,$var);

直接解析 query 就简单多了,直接放入 $var 向后流转。

path 的最后解析

第一种模式和第二种模式都会解析出 path。这里对 path 又进行了一次操作。

<?php
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);
    }
}

这里有段逻辑,会从 path 解析的数组最后开始弹出,第一个弹出的是 action,然后 controllermodule,如果某个环节发现 path 数组被弹空之后,后续的就无法再赋值了。

parseRegex 解析

通过一个例子来看一下整个流程。

假设规则如下:

// 规则
'URL_ROUTE_RULES' => [
    '/^blog\/(\w+)$/' => 'Blog/read/id/:1'
]

请求链接是 http://demo.com/blog/123/page/1

在执行 parseRegex 之前会根据正则 /^blog\/(\w+)$/ 匹配出如下结果:

array(2) {
  [0]=>
  string(8) "blog/123"
  [1]=>
  string(3) "123"
}

接下来执行 parseRegex

<?php
$url   =  is_array($route)?$route[0]:$route;
$url   =  preg_replace_callback('/:(\d+)/', function($match) use($matches){return $matches[$match[1]];}, $url); 

获取路由 Blog/read/id/:1,通过 preg_replace_callback:1 替换成 123。最终获得 $urlBlog/read/id/123

<?php
if(0=== strpos($url,'/') || 0===strpos($url,'http')) { // 路由重定向跳转
    header("Location: $url", true,(is_array($route) && isset($route[1]))?$route[1]:301);
    exit;
}

如果匹配出来的 $url/ 或者 http 开头,则会执行跳转...(很神奇的操作,也很坑,万一没注意加了一个 /,还真不一定能够很快定位问题在哪里)。

<?php
// 解析路由地址
$var  =  self::parseUrl($url);

如果不跳转,就开始解析 $url 获得参数。

此时的 $var

array(4) {
  ["a"] => string(3) "123"
  ["m"] => string(2) "id"
  ["g"] => string(4) "read"
}
<?php
// 处理函数
foreach($var as $key=>$val){
    if(strpos($val,'|')){
        list($val,$fun) = explode('|',$val);
        $var[$key]    =   $fun($val);
    }
}

参数获得之后,会去解析每个参数,解析执行 函数过滤处理

<?php
// 解析剩余的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);
}

解析剩余的URL参数 /page/1,并将参数给 $var

此时的 $var

array(4) {
  ["a"] => string(3) "123"
  ["m"] => string(2) "id"
  ["g"] => string(4) "read"
  ["page"] => string(1) "1"
}
<?php
// 解析路由自动传入参数
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);

解析路由剩下的部分,并且与 $var 合并。最后赋值给 $_GET

invokeRegx 解析

// 执行正则匹配下的闭包方法 支持参数调用
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);
}
<?php
$reflect = new \ReflectionFunction($closure);
$params  = $reflect->getParameters();

通过反射获取到函数的实体和参数。

<?php
array_shift($var);

因为 $var 是正则匹配后的结果,值如下:

array(2) {
  [0] => string(8) "blog/123"
  [1] => string(3) "123"
}

而下标为0的没有作用,这里直接将其从数组里弹出。

<?php
foreach ($params as $param){
    if(!empty($var)) {
        $args[] = array_shift($var);
    }elseif($param->isDefaultValueAvailable()){
        $args[] = $param->getDefaultValue();
    }
}

遍历参数,并拼凑参数。

如果 $var 里有值则按照顺序弹出。如果没有,则获取参数的默认值。

<?php
return $reflect->invokeArgs($args);

最后执行函数。

checkUrlMatch 解析

假设规则如下:

'URL_ROUTE_RULES' => [
    'blog/:id/[:page\d]'   => 'Blog/read/id/:1'
]

请求链接为 blog/123/1

<?php
$m1 = explode('/',$regx);
$m2 = explode('/',$rule);
$var = array();

将路由和规则都解析成数组,然后依据规则的数组开始逐个解析。

解析后的结果如下:

// m1
array(3) {
  [0] => string(4) "blog"
  [1] => string(3) "123"
  [2] => string(1) "1"
}
// m2
array(3) {
  [0] => string(4) "blog"
  [1] => string(3) ":id"
  [2] => string(13) "[:page\d]"
}
<?php
if(0 === strpos($val,'[:')){
    $val    =   substr($val,1,-1);
}

如果是可选的,那么会移除掉两边的东西 [],保留剩余的部分。例如:[:page\d] -> :page\d

if(':' == substr($val,0,1)) {
} else if (0 !== strcasecmp($val,$m1[$key])){
    return false;
}

如果不是以 : 开头,并且在 $m1 相同的位置的值一致(忽略大小写),则表明正常,否则返回错误。例如:blog

<?php
if($pos = strpos($val,'|')){
    // 使用函数过滤
    $val   =   substr($val,1,$pos-1);
}

如果是以 : 开头的,则会检测是否存在 |,如果存在则截取。例如::page^1|2|3 -> :page^1。但是这个和后面的匹配规则冲突,所以在 3.2.2 中,将 | 改为了 -。(这个版本中,不建议使用,因为没用)

<?php
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);
}

如果检测到字符串里存在 \,并且不在首位,则会获取类型,这里只支持一位(很重要)。

如果还是数字的话 d,会从 $m1 中获取相应的值,并匹配类型。

<?php
if($pos = strpos($val,'^')){
    $array   =  explode('|',substr(strstr($val,'^'),1));
    if(in_array($m1[$key],$array)) {
        return false;
    }
    $name = substr($val, 1, $pos - 1);
}

3.2.0 版本中,这个可以忽略,和上面说道的地方冲突了,不好用。

<?php
$name = substr($val, 1);

如果不存在 \^,则直接截取。例如::id -> id

<?php
return $var;

最后返回 $var

invokeRule 解析

invokeRegx 差不多,不同的地方在于 $var 的结构不同。

小伙伴们可以参照上面的解析来看这段源码。

parseRule 解析

这里和上面的 parseRegex 有很多类似的地方,可以调试查看,这里就不重复太多了。

最后

TP 的路由写的比较复杂,适合的情况也有很多,但是限制也不少,需要了解的情况太多,而且还存在不少坑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容