Stay hungry, Stay foolish

0%

TP笔记6:URL路由

曾经和一朋友聊框架的时候,总结道:

框架其实就是MVC结构、URL路由、数据库驱动、模板引擎的结合。

想来想去也确实没什么东西。由此可见URL在框架里的地位,但是说它重要,它又不是那么重要,为什么呢?

第一,它没有多少,第二,它的作用也就是美化URL。仅此而已。

TP里提供了四种URL模式,在之前接触的框架定义文件defines.php里有它们的说明:

1
2
3
4
5
//支持的URL模式
define('URL_COMMON',      0);   //普通模式
define('URL_PATHINFO',    1);   //PATHINFO模式
define('URL_REWRITE',     2);   //REWRITE模式
define('URL_COMPAT',      3);   // 兼容模式
  • 普通模式
1
http://localhost/appName/index.php?m=moduleName&a=actionName&id=1

TP里默认m是model,a是action,g是group,在框架配置文件convention.php里定义:

1
2
3
4
5
6
7
8
9
'VAR_GROUP'             => 'g',     // 默认分组获取变量
'VAR_MODULE'            => 'm',        // 默认模块获取变量
'VAR_ACTION'            => 'a',        // 默认操作获取变量
'VAR_ROUTER'            => 'r',     // 默认路由获取变量
'VAR_PAGE'              => 'p',        // 默认分页跳转变量
'VAR_TEMPLATE'          => 't',        // 默认模板切换变量
'VAR_LANGUAGE'          => 'l',        // 默认语言切换变量
'VAR_AJAX_SUBMIT'       => 'ajax'// 默认的AJAX提交变量
'VAR_PATHINFO'          => 's',    // PATHINFO 兼容模式获取变量

在这里可以修改成自己喜欢的

  • PATHINFO模式(TP默认)
1
http://localhost/appName/index.php/moduleName/actionName/id/1/
  • REWRITE模式
1
http://localhost/appName/moduleName/actionName/id/1/

与pathinfo模式相比隐藏了入口文件index.php,用web服务器(apache、nginx等)来配置。

+ 兼容模式

1
http://localhost/appName/?s=/module/action/id/1/

服务器不能很好地支持PATHINFO模式而又不想用普通模式可以试试这个。

用现在流行的话来说:

用普通模式的是普通青年,用PATHINFO和REWRITE的是文艺青年,用兼容模式的是2B青年。呵呵。开个玩笑。

接下来看看TP是如何实现来着的吧。

Dispatcher一共有四个方法:

  • dispatch URL路由处理

  • getPathInfo 获取URL的pathInfo

  • parsePathInfo 解析pathInfo

  • routerCheck 检查路由是否合法

要想对URL进行处理,首先要获取它并解析:

getPathInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if(!empty($_GET[C('VAR_PATHINFO')])) {
// 兼容PATHINFO 参数
$path = $_GET[C('VAR_PATHINFO')];
unset($_GET[C('VAR_PATHINFO')]);
}elseif(!empty($_SERVER['PATH_INFO'])){
$pathInfo = $_SERVER['PATH_INFO'];
if(0 === strpos($pathInfo,$_SERVER['SCRIPT_NAME']))
$path = substr($pathInfo, strlen($_SERVER['SCRIPT_NAME']));
else
$path = $pathInfo;
}elseif(!empty($_SERVER['ORIG_PATH_INFO'])) {
$pathInfo = $_SERVER['ORIG_PATH_INFO'];
if(0 === strpos($pathInfo, $_SERVER['SCRIPT_NAME']))
$path = substr($pathInfo, strlen($_SERVER['SCRIPT_NAME']));
else
$path = $pathInfo;
}elseif (!empty($_SERVER['REDIRECT_PATH_INFO'])){
$path = $_SERVER['REDIRECT_PATH_INFO'];
}elseif(!empty($_SERVER["REDIRECT_Url"])){
$path = $_SERVER["REDIRECT_Url"];
if(empty($_SERVER['QUERY_STRING']) || $_SERVER['QUERY_STRING'] == $_SERVER["REDIRECT_QUERY_STRING"])
{
$parsedUrl = parse_url($_SERVER["REQUEST_URI"]);
if(!empty($parsedUrl['query'])) {
$_SERVER['QUERY_STRING'] = $parsedUrl['query'];
parse_str($parsedUrl['query'], $GET);
$_GET = array_merge($_GET, $GET);
reset($_GET);
}else {
unset($_SERVER['QUERY_STRING']);
}
reset($_SERVER);
}
}

这一段多分支结构,只是为了获取path信息:

  • 检查$_GET里是否包含VAR_PATHINFO(兼容模式的获取变量,默认是s),保存到$path变量里,释放$_GET[s],否则进入条件2;(这一条是为兼容模式准备的)

  • 检查$_SERVER[‘PATH_INFO’],从里面去掉$_SERVER[‘SCRIPT_NAME’]。

$_SERVER[‘PATH_INFO’]获取的是.php后缀后面的内容,$_SERVER[‘SCRIPT_NAME’]获取的是域名之后的第一个斜杠(/)到.php(包含.php)之间的内容。

(这一条是针对PATHINFO和REWRITE模式的)

否则进入下一个条件分支;

  • 判断$_SERVER[‘ORIG_PATH_INFO’]

网上说$_SERVER[‘ORIG_PATH_INFO’]相当于$_SERVER[‘PATH_INFO’]的原始信息,$_SERVER[‘PATH_INFO’]在localhost下显示,而$_SERVER[‘ORIG_PATH_INFO’]在服务器上会显示,这个到目前还没有得到证实。

  • $_SERVER[‘REDIRECT_PATH_INFO’]是用header()进行跳转后的PATH_INFO。

  • $_SERVER[“REDIRECT_Url”]

1
2
3
4
5
if(C('URL_HTML_SUFFIX') && !empty($path)) {
$suffix = substr(C('URL_HTML_SUFFIX'),1);
$path = preg_replace('/.'.$suffix.'$/','',$path);
}
$_SERVER['PATH_INFO'] = empty($path) ? '/' : $path;

URL_HTML_SUFFIX是URL伪静态的设置,在获取到path后,会在后面加上设置的后缀,默认是空。

最后把path写到$_SERVER[‘PATH_INFO’]中。

parsePathInfo

1
$pathInfo = array();

先定义一个数组用来存解析后的URL信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

if(C('URL_PATHINFO_MODEL')==2){

'URL_PATHINFO_MODEL'1是普通模式,2是智能模式(PATHINFO和REWRITE),3是兼容模式
[php]
$paths = explode(C('URL_PATHINFO_DEPR'),trim($_SERVER['PATH_INFO'],'/'));
$groupApp = C('APP_GROUP_LIST');
if ($groupApp) {
$arr = array_map('strtolower',explode(',',$groupApp));
$pathInfo[C('VAR_GROUP')] = in_array(strtolower($paths[0]),$arr)? array_shift($paths) : '';
}
$pathInfo[C('VAR_MODULE')] = array_shift($paths);
$pathInfo[C('VAR_ACTION')] = array_shift($paths);
for($i = 0, $cnt = count($paths); $i <$cnt; $i++){
if(isset($paths[$i+1])) {
$pathInfo[$paths[$i]] = (string)$paths[++$i];
}elseif($i==0) {
$pathInfo[$pathInfo[C('VAR_ACTION')]] = (string)$paths[$i];
}
}

刚才处理的$_SERVER[‘PATH_INFO’]在这里开始发光发热。把它左右的斜杠(/)给删除掉后,进行分隔(按URL_PATHINFO_DEPR设置的URL路径分隔符),存入$path; 读取项目的分组列表:$groupApp = C(‘APP_GROUP_LIST’); 如果$path[0]在项目的分组列表里,就把它写到$pathInfo里:

1
$pathInfo[C('VAR_GROUP')] = in_array(strtolower($paths[0]),$arr)? array_shift($paths) : '';

上面已经列举了,框架默认的VAR_GROUP是g,翻译过来就是:

1
$pathInfo[g] = in_array(strtolower($paths[0]),$arr)? array_shift($paths) : '';

接着读取模块和动作:

1
2
$pathInfo[C('VAR_MODULE')] = array_shift($paths);
$pathInfo[C('VAR_ACTION')] = array_shift($paths);

接下来把剩余的遍历以写入$pathInfo:如果剩下的都是成对的,就以key(i)=>value(i+1)的形式写入。这里有一个特殊情况就是$path里只剩一个值了(i=0有值,i=1没有),那么就把这个值赋给VAR_ACTION对应的value。如:

1
http://domain.com/index.php/GroupName/ModuleName/ActionName/123

解析后的:

1
2
3
4
$pathInfo['g'] = GroupName;
$pathInfo['m'] = ModuleName;
$pathInfo['a'] = ActionName;
$pathInfo[ActionName] = 123;

这个有什么用?

在dispath方法里调用它把$pathInfo的信息合并到$_GET中:

1
2
$_GET = array_merge(self :: parsePathInfo(),$_GET);
$_REQUEST = array_merge($_POST,$_GET);

然后在App的getGroup、getModule和getAction中调用,看一下getModule里的内容:

1
2
3
4
5
6
7
8
9
10
11
12
$var  =  C('VAR_MODULE');
$module = !empty($_POST[$var]) ?
$_POST[$var] :
(!empty($_GET[$var])? $_GET[$var]:C('DEFAULT_MODULE'));
if(C('URL_CASE_INSENSITIVE')) {
// URL地址不区分大小写
define('P_MODULE_NAME',strtolower($module));
// 智能识别方式 index.php/user_type/index/ 识别到 UserTypeAction 模块
$module = ucfirst(parse_name(P_MODULE_NAME,1));
}
unset($_POST[$var],$_GET[$var]);
return $module;

从$_GET和$_POST里判断是否有值,没有的话取默认值。然后在App::exec中实例化。(这就是框架里通过URL来执行控制器里方法的秘密)。

回到Dispatcher,该看看dispatch方法了:

dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
$urlMode  =  C('URL_MODEL');
if($urlMode == URL_REWRITE ) {
//当前项目地址
$url = dirname(_PHP_FILE_);
if($url == '/' || $url == '')
$url = '';
define('PHP_FILE',$url);
}elseif($urlMode == URL_COMPAT){
define('PHP_FILE',_PHP_FILE_.'?'.C('VAR_PATHINFO').'=');
}else {
//当前项目地址
define('PHP_FILE',_PHP_FILE_);
}

取出当前项目的URL_MODEL,根据MODEL来定义项目的地址PHP_FILE。兼容模式的PHP_FIEL是当前项目+C(‘VAR_PATHINFO’)+等号。接下来又是对$urlMode的判断:

1
2
3
4
if($urlMode) {
}else{
if(isset($_GET[C('VAR_ROUTER')])) self::routerCheck();
}

前面已经知道$urlMode只有四个值:0-3,那么在这时,只有$urlMode为0,即普通模式,会走到else分支后的部分:除了路由检查什么也不用干,当然,路由检查还要在开启的状态下。 而对于其它的模式,就要做些处理了。

1
2
self::getPathInfo();
if (!empty($_GET) && !isset($_GET[C('VAR_ROUTER')])) {

对当前的URL进行处理,然后判断$_GET是否为空,以及$_GET[C(‘VAR_ROUTER’)])是否设置。

1
2
3
4
5
6
$_GET  =  array_merge (self :: parsePathInfo(),$_GET);
$_varGroup = C('VAR_GROUP'); // 分组变量
$_varModule = C('VAR_MODULE');
$_varAction = C('VAR_ACTION');
$_depr = C('URL_PATHINFO_DEPR');
$_pathModel = C('URL_PATHINFO_MODEL');

取出处理后的PATHINFO信息,与$_GET合并;

读取一些配置:分组、模块、动作的默认变量,URL分隔符,PATHINFO模式。

1
2
3
4
5
6
if (!C('APP_GROUP_LIST')) {
$_GET[$_varGroup] = '';
}
// 设置默认模块和操作
if(empty($_GET[$_varModule])) $_GET[$_varModule] = C('DEFAULT_MODULE');
if(empty($_GET[$_varAction])) $_GET[$_varAction] = C('DEFAULT_ACTION');

读取分组名的列表,如果为空,就把分组变量设为空;

判断解析后的url info里是否有模块名和动作名,如果没有,取默认的。

下面开始重新组装URL:

1
$_URL = '/'; if($_pathModel==2) {     $_URL .= $_GET[$_varGroup].($_GET[$_varGroup]?$_depr:'').$_GET[$_varModule].$_depr.$_GET[$_varAction].$_depr;     unset($_GET[$_varGroup],$_GET[$_varModule],$_GET[$_varAction]); }

如果URL_PATHINFO_MODEL是2(PATHINFO、REWRITE),判断Group、Module及Action,并把它们用设置的分隔符给连接起来。最后,从$_GET中删除。

1
2
3
4
5
6
7
8
9
foreach ($_GET as $_VAR => $_VAL) {
if('' != trim($_GET[$_VAR])) {
if($_pathModel==2) {
$_URL .= $_VAR.$_depr.rawurlencode($_VAL).$_depr;
}else{
$_URL .= $_VAR.'/'.rawurlencode($_VAL).'/';
}
}
}

把剩余的$_GET里的信息连接起来。

1
2
if($_depr==',') $_URL = substr($_URL, 0, -1).'/';
redirect(PHP_FILE.$_URL);

在Url模式的PATHINFO和REWRITE两种模式里,存在着另一种默认的URL分隔符,即英文半角逗号(,):

智能模式 设置URL_PATHINFO_MODEL参数为2 (系统默认的模式)自动识别模块和操作,例如 http://serverName/appName/module/action/id/1/ 或者 http://serverName/appName/module,action,id,1/ 在智能模式下面,第一个参数会被解析成模块名称(或者路由名称,下面会有描述), 第二个参数会被解析成操作(在第一个参数不是路由名称的前提下)

跳转到组装好的URL,URL重写完成。

如果不需要重写:

1
2
3
if(C('URL_ROUTER_ON')) self::routerCheck();
$_GET = array_merge(self :: parsePathInfo(),$_GET);
$_REQUEST = array_merge($_POST,$_GET);

判断是否检查路由规则,合并$_GET、$REQUEST。

据说打赏我的人,代码没有BUG