这里是用
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
)
接下来可以分别解析 path
和 query
,并通过 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
,然后 controller
和 module
,如果某个环节发现 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
。最终获得 $url
为 Blog/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
的路由写的比较复杂,适合的情况也有很多,但是限制也不少,需要了解的情况太多,而且还存在不少坑。