xts路由器
单入口PHP和路由
早期的PHP程序都使用CGI方式实现,根据Web服务器上定义的网站根目录找到对应的PHP文件开始执行,于是文件路径也就成为PHP程序的天然路由器。访问不同的URL,自然会运行不同的PHP程序,彼此隔离,互不干扰,就正是多入口程序系统。不过由于的确存在一些需要复用的代码,比如系统配置,公共函数等等,所以PHP提供了include或者require方式来引入另一个文件PHP文件。这样还是存在一个问题,就是几乎所有的PHP文件都要先include一下,非常不方便。后来PHP5时代提供了autoload方法,让大家可以不必四处写include了。不过autoload的性能实在不怎么样,每次调用它都需要扫描一遍inlcude_path,才能正确地把文件引入,再说它只能支持类引入,对函数就无能为力。
后来兴起的众多PHP框架都开始使用单入口方式。所有的请求都被Web服务器rewrite到一个PHP入口文件中,然后由它完成程序初始化工作,最后按照配置规则加载和运行开发人员编写代码。这样程序会有一个集中的地方加载需要的模块,而且能很自由地定义URL结构,甚至把一部分参数作为URL的一部分。
apple组件
xts提供了一个名为apple的组件来管理程序的路由入口。在protected/config/debug.php
的组件配置部分可以找到如下片段:
'apple' => array(
'class' => '\\xts\\Apple',
'singleton' => true,
'conf' => array(
'actionDir' => X_PROJECT_ROOT.'/protected/action',
'defaultAction' => '/index',
'actionPrefix' => 'action_',
'preAction' => '',
'preActionFile' => '',
),
),
apple组件是一个xts\Apple
类的单件实例,它除了提供路由功能之外,还提供程序应用程序终止、页面跳转以及JSON输出等支持功能。
action函数
我在为xts设计路由功能的时候,希望能尽可能简单一点,所以没有像其它MVC框架那样使用Controller/View两层结构,也没有提供基于正则表达式的路由规则配置。正如前面马上开始里Hello world的例子,xts路由会直接从protected/action
目录里寻找最接近的匹配文件载入,并调用同名的函数作为action函数。这里的protected/action
目录是被apple组件actionDir
配置项定义的,如果修改此配置项的地址,可以将action函数文件放在其它地方。
xts路由功能会完成下面的几个步骤:
- 在actionDir中查找和引入匹配的PHP文件;
- 确定action函数
- 调用action函数
路由规则
xts的路由查找是根据$_SERVER['REQUEST_URI']变量进行的,在配置actionDir定义的目录里寻找,尽最大可能匹配目录和文件。xts在进行路由查找的时候,会去掉URL上的文件扩展名和参数,所以对于xts来说/a/b/c.html
和/a/b/c.htm
以及/a/b/c.php?id=5
的查找规则是一样的,都会当作/a/b/c
来查找对应的文件。
我们以请求/a/b/c/d
来说明xts的查找过程,xts会先尝试加载{{actionDir}}/a/b/c/d.php
这个文件,如果此文件不存在,xts会再尝试{{actionDir}}/a/b/c.php
,如果仍未找到,会再尝试{{actionDir}}/a/b.php
,最后,xts会尝试{{actionDir}}/a.php
。如果xts没有找到可以匹配的文件,就不会引入任何的文件。这意味着用户可以不把action函数写在路由规则对应的文件里。当这种情况发生时,xts会把URI最后一级的名称作为action函数的名称,前面的例子中会以d
作为action函数名。
action函数前綴
在前面按规则路由的过程中,xts已经确定了action函数的基础名称,就是被引入文件的主文件名。不过有不少URL中常用的英文单词被列为PHP语言保留字,或者正好有同名的built-in函数存在。比如想在列表页使用list这个名字,可是list
是PHP语言的保留字,用于将数组的值按位置赋给变量;再比如某个WebServiceAPI统计内容数量的接口想使用count,但count
是PHP的内建函数,用于统计数组的元素个数。为了解决这一冲突的问题,xts允许为action函数在基础名称前面加一个前缀,路由的时候还是按前面的规则找匹配的文件,文件中的函数名则优先使用带前缀的版本。action函数的前缀在配置项actionPrefix
中定义,默认是action_
。
有了action函数前缀的帮助,我们就可以在{{actionDir}}/article/list.php
里定义action_list
这个函数,xts会在用户访问/article/list
的时候调用action_list
函数。action函数前缀定义之后并不是非加不可,对于没有冲突的情况,依旧可以使用和文件名相同的基础名称作为函数名。比如用户访问URI /article/show
的时候,还是可以在{{actionDir}}/article/show.php
文件里定义名为show
的函数,xts也会正确地调用它。带前缀的函数名优先于不带前缀的函数,所以如果同时定义了带前缀和不带前缀的两个函数,xts只会调用带前缀的那个。
不必担心用户会利用路由规则调用PHP的内建函数,xts会阻止所有的这种请求
智能参数映射
xts支持将GET参数映射到action函数的参数表中。比如设计用户请求的URI是/article/list?page=3&category=diary
,那么action_list
函数可以定义接收参数page和category。
function action_list($category='default', $page=3) {
// 列出文章列表
}
这个例子中,在action_list函数内可以直接使用$page和$category参数,xts会把$_GET中的同名参数直接赋值进来。因为是按名字赋值,所以和用户访问时的参数顺序无关,请求URI改成/article/list?category=diary&page=3
和前面是等价的。
如果action函数声明的参数不可省略,而请求的$_GET数组又没有同名参数,那么xts会返回一个400错误
除了命名参数可以自动赋值外,xts现在还支持另一种给action函数的参数赋值的方式——基于位置的参数。还是以例子说明,如果设计网站的用户个人主页URI是这样的格式/user/{{name}}
,其中{{name}}
是用户的名字。那么可以在{{actionDir}}/user.php
中定义user函数,接收name参数:
function user($name) {
// 加载用户信息
}
基于位置的参数也可以支持多个参数,比如URI /article/list/diary/3
可以对应到action_list($category, $page)
函数上,diary是第一个参数,3是第二个参数。
注意:基于位置的参数和命名参数映射不能混合使用
fallback_action
如果依前面方法路由确定的action函数没有定义,xts会检查是否存在名为fallback_action
的函数,如果存在就调用它。fallback_action
可以用来输出自定义的404页面。或者根据请求给出一些提示。如果fallback_action
也没有定义,xts会直接返回HTTP状态码404,并以404 Not Found作为页面内容输出。
preAction函数
经常会有一些需要在正式逻辑开始前的准备工作,比如检查用户的登录状态,检查权限什么的。preAction就是在所有action执行前运行的一个函数。这个函数存放的文件位置由preActionFile配置项定义,函数的名称由preAction配置项定义。这两个配置默认为空,表示不启用此功能。下面是一个检查用户登录的例子:
function check_login($action) {
$guestAllowed = array('/index','/login','/register',);
if (!in_array($action, $guestAllowed)) {
$user = X::orange('user')->load($_SESSION['user_id']);
if($user instanceof User)
$_SESSION['current_user'] = $user;
else
X::apple()->redirect('/login')->end();
}
}
页面跳转
页面跳转是很常用的功能,一般需要自己写一个header输出,xts提供了一个简单的封装,即xts\Apple::redirect
函数。在前一小节preAction函数的例子中已经使用到了。除了可以指定跳转的地址外,redirect方法还可以接收第二个参数$statusCode
。第二个是可选的参数,用于指定服务器的返回状态码,默认情况下是302。redirect函数返回Apple对象自身,支持链式调用。
终止程序运行
在action函数中可以直接return来终止程序的运行。如果在其它地方,可以调用X::apple()->end()
方法。不建议使用PHP的die或者exit来结束程序,主要是出于兼容性和未来进行性能评估方便的考虑。调用end方法终止程序之后,index.php里后续的程序还是会运行的,比如设置性能评估终止标记,写入日志等等。