利用路由功能,可以让你的URL地址更加简洁和优雅。ThinkPHP支持对模块的URL地址进行路由操作(路由功能是针对PATHINFO模式或者兼容URL而设计的,暂时不支持普通URL模式)。
ThinkPHP的路由功能包括:
- 正则路由
- 规则路由
- 静态路由(URL映射)
- 闭包支持
# 路由定义
## 启用路由
要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情况下不支持路由功能),并且在应用(或者模块)配置文件中开启路由:
~~~
// 开启路由
'URL_ROUTER_ON' => true,
~~~
> 路由功能可以针对模块,也可以针对全局,针对模块的路由则需要在模块配置文件中开启和设置路由,如果是针对全局的路由,则是在公共模块的配置文件中开启和设置(后面我们以模块路由定义为例)。
然后就是配置路由规则了,在模块的配置文件中使用**URL_ROUTE_RULES**参数进行配置,配置格式是一个数组,每个元素都代表一个路由规则,例如:
~~~
'URL_ROUTE_RULES'=>array(
'news/:year/:month/:day' => array('News/archive', 'status=1'),
'news/:id' => 'News/read',
'news/read/:id' => '/news/:1',
),
~~~
系统会按定义的顺序依次匹配路由规则,一旦匹配到的话,就会定位到路由定义中的控制器和操作方法去执行(可以传入其他的参数),并且后面的规则不会继续匹配。
## 路由定义
路由规则的定义格式为: **'路由表达式'=>'路由地址和传入参数'**
或者:**array('路由表达式','路由地址','传入参数')**
> 模块路由和全局路由配置的区别在于,全局路由的路由地址必须包含模块。
##### 路由表达式
路由表达式包括规则路由和正则路由的定义表达式,只能使用字符串。
| 表达式 | 示例 |
|-----|-----|
| 正则表达式 | /^blog\/(\d+)$/ |
| 规则表达式 | blog/:id |
> 详细的规则路由和正则路由表达式的定义方法参考后面的章节。
##### 路由地址
路由地址(可以支持传入额外参数)表示前面的路由表达式需要路由到的地址(包括内部地址和外部地址),并且允许隐式传入URL里面没有的一些参数,这里允许使用字符串或者数组方式定义,特殊情况下还可以采用闭包函数定义路由功能,支持下面6种方式定义:
| 定义方式 | 定义格式 |
|-----|-----|
| 方式1:路由到内部地址(字符串) | '[控制器/操作]?额外参数1=值1&额外参数2=值2...' |
| 方式2:路由到内部地址(数组)参数采用字符串方式 | array('[控制器/操作]','额外参数1=值1&额外参数2=值2...') |
| 方式3:路由到内部地址(数组)参数采用数组方式 | array('[控制器/操作]',array('额外参数1'=>'值1','额外参数2'=>'值2'...)[,路由参数]) |
| 方式4:路由到外部地址(字符串)301重定向 | '外部地址' |
| 方式5:路由到外部地址(数组)可以指定重定向代码 | array('外部地址','重定向代码'[,路由参数]) |
| 方式6:闭包函数 | function($name){ echo 'Hello,'.$name;} |
如果你定义的是全局路由(在公共模块的配置文件中定义),那么路由地址的定义格式中需要增加模块名,例如:
~~~
'blog/:id'=>'Home/blog/read' // 表示路由到Home模块的blog控制器的read操作方法
~~~
如果路由地址以“/”或者“http”开头则会认为是一个重定向地址或者外部地址,例如:
~~~
'blog/:id'=>'/blog/read/id/:1'
~~~
和
~~~
'blog/:id'=>'blog/read'
~~~
虽然都是路由到同一个地址,但是前者采用的是301重定向的方式路由跳转,这种方式的好处是URL可以比较随意(包括可以在URL里面传入更多的非标准格式的参数),而后者只是支持模块和操作地址。
举个例子,如果我们希望 `avatar/123` 重定向到 `/member/avatar/id/123_small` 的话,只能使用:
~~~
'avatar/:id'=>'/member/avatar/id/:1_small'
~~~
路由地址采用重定向地址的话,如果要引用动态变量,也是采用 `:1、:2` 的方式。
采用重定向到外部地址通常对网站改版后的URL迁移过程非常有用,例如:
~~~
'blog/:id'=>'http://blog.thinkphp.cn/read/:1'
~~~
表示当前网站(可能是http://thinkphp.cn)的 `blog/123` 地址会直接重定向到 `http://blog.thinkphp.cn/read/123`。
默认情况下,外部地址的重定向采用301重定向,如果希望采用其它的,可以使用:
~~~
'blog/:id'=>array('http://blog.thinkphp.cn/read/:1',302);
~~~
在路由跳转的时候支持额外传入参数对(额外参数指的是不在URL里面的参数,隐式传入需要的操作中,有时候能够起到一定的安全防护作用,后面我们会提到),支持 `额外参数1=值1&额外参数2=值2` 或者 `array('额外参数1'=>'值1','额外参数2'=>'值2'...)` 这样的写法,可以参考不同的定义方式选择。例如:
~~~
'blog/:id'=>'blog/read?status=1&app_id=5',
'blog/:id'=>array('blog/read?status=1&app_id=5'),
'blog/:id'=>array('blog/read','status=1&app_id=5'),
'blog/:id'=>array('blog/read',array('status'=>1,'app_id'=>5)),
~~~
上面的路由规则定义中额外参数的传值方式都是等效的。`status`和`app_id`参数都是URL里面不存在的,属于隐式传值,当然并不一定需要用到,只是在需要的时候可以使用。
## 路由参数
当路由地址采用数组方式定义的时候,还可以传入额外的路由参数。
> 这些参数的作用是限制前面定义的路由规则的生效条件。
##### 限制URL后缀
例如:
~~~
'blog/:id'=>array('blog/read','status=1&app_id=5',array('ext'=>'html')),
~~~
就可以限制html后缀访问该路由规则才能生效。
##### 限制请求类型
例如:
~~~
'blog/:id'=>array('blog/read','status=1&app_id=5',array('method'=>'get')),
~~~
就限制了只有GET请求该路由规则才能生效。
##### 自定义检测
支持自定义检测,例如: 例如:
~~~
'blog/:id'=>array('blog/read','status=1&app_id=5',array('callback'=>'checkFun')),
~~~
就可以自定义checkFun函数来检测是否生效,如果函数返回false则表示不生效。
# 规则路由
规则路由是一种比较容易理解的路由定义方式,采用ThinkPHP设计的规则表达式来定义。
## 规则表达式
规则表达式通常包含静态地址和动态地址,或者两种地址的结合,例如下面都属于有效的规则表达式:
~~~
'my' => 'Member/myinfo', // 静态地址路由
'blog/:id' => 'Blog/read', // 静态地址和动态地址结合
'new/:year/:month/:day'=>'News/read', // 静态地址和动态地址结合
':user/:blog_id' =>'Blog/read',// 全动态地址
~~~
> 规则表达式的定义始终以“/”为参数分割符,不受`URL_PATHINFO_DEPR`设置的影响
每个参数中以“:”开头的参数都表示动态参数,并且会自动对应一个GET参数,例如`:id`表示该处匹配到的参数可以使用`$_GET['id']`方式获取,`:year`、 `:month` 、`:day` 则分别对应`$_GET['year']`、 `$_GET['month']` 和 `$_GET['day']`。
##### 数字约束
支持对变量的类型检测,但仅仅支持数字类型的约束定义,例如
~~~
'blog/:id\d'=>'Blog/read',
~~~
表示只会匹配数字参数,如果你需要更加多的变量类型检测,请使用正则表达式定义来解决。
> 目前不支持长度约束,需要的话采用正则定义解决
##### 函数支持
可以支持对路由变量的函数过滤,例如:
~~~
'blog/:id\d|md5'=>'Blog/read',
~~~
表示对匹配到的id变量进行md5处理,也就是说,实际传入read操作方法的`$_GET['id']` 其实是 `md5($_GET['id'])`。
> 注意:不支持对变量使用多次函数处理和函数额外参数传入。
##### 可选定义
支持对路由参数的可选定义,例如:
~~~
'blog/:year\d/[:month\d]'=>'Blog/archive',
~~~
`[:month\d]`变量用[ ]包含起来后就表示该变量是路由匹配的可选变量。
以上定义路由规则后,下面的URL访问地址都可以被正确的路由匹配:
~~~
http://serverName/index.php/Home/blog/2013
http://serverName/index.php/Home/blog/2013/12
~~~
采用可选变量定义后,之前需要定义两个或者多个路由规则才能处理的情况可以合并为一个路由规则。
> 可选参数只能放到路由规则的最后,如果在中间使用了可选参数的话,后面的变量都会变成可选参数。
##### 规则排除
非数字变量支持简单的排除功能,主要是起到避免解析混淆的作用,例如:
~~~
'news/:cate^add-edit-delete'=>'News/category'
~~~
因为规则定义的局限性,恰巧我们的路由规则里面的news和实际的news模块是相同的命名,而`:cate`并不能自动区分当前URL里面的动态参数是实际的操作名还是路由变量,所以为了避免混淆,我们需要对路由变量cate进行一些排除以帮助我们进行更精确的路由匹配,格式`^add-edit-delete` 表示,匹配除了add edit 和delete之外的所有字符串,我们建议更好的方式还是改进你的路由规则,避免路由规则和模块同名的情况存在,例如
~~~
'new/:cate'=>'News/category'
~~~
就可以更简单的定义路由规则了。
##### 完全匹配
规则匹配检测的时候只是对URL从头开始匹配,只要URL地址包含了定义的路由规则就会匹配成功,如果希望完全匹配,可以使用$符号,例如:
~~~
'new/:cate$'=> 'News/category'
~~~
`http://serverName/index.php/Home/new/info`
会匹配成功,而
`http://serverName/index.php/Home/new/info/2`
则不会匹配成功。
如果是采用
~~~
'new/:cate'=> 'News/category'
~~~
方式定义的话,则两种方式的URL访问都可以匹配成功。
> 完全匹配的路由规则中如果使用可选参数的话将会无效。
# 正则路由
正则路由也就是采用正则表达式定义路由的一种方式,依靠强大的正则表达式,能够定义更灵活的路由规则。
路由表达式支持的正则定义必须以“/”开头,否则就视为规则表达式。也就是说如果采用:
~~~
'#^blog\/(\d+)$#' => 'Blog/read/id/:1'
~~~
方式定义的正则表达式不会被支持,而会被认为是规则表达式进行解析,从而无法正确匹配。
下面是一种正确的正则路由定义:
~~~
'/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1&month=:2',
~~~
> 对于正则表达式中的每个变量(即正则规则中的子模式)部分,如果需要在后面的路由地址中引用,可以采用:1、:2这样的方式,序号就是子模式的序号。
正则定义也支持函数过滤处理,例如:
~~~
'/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1|format_year&month=:2',
~~~
其中 year=:1|format_year 就表示对匹配到的变量进行format_year函数处理(假设format_year是一个用户自定义函数)。
更多的关于如何定义正则表达式就不在本文的描述范畴了。
# 静态路由
静态路由其实属于规则路由的静态简化版(又称为URL映射),路由定义中不包含动态参数,静态路由不需要遍历路由规则而是直接定位,因此效率较高,但作用也有限。
如果我们定义了下面的静态路由
~~~
'URL_ROUTER_ON' => true,
'URL_MAP_RULES'=>array(
'new/top' => 'news/index?type=top'
)
~~~
> 注意:为了不影响动态路由的遍历效率,静态路由采用URL_MAP_RULES定义和动态路由区分开来
定义之后,如果我们访问: `http://serverName/Home/new/top`
其实是访问: `http://serverName/Home/news/index/type/top`
静态路由是完整匹配,所以如果访问: `http://serverName/Home/new/top/var/test`
尽管前面也有`new/top`,但并不会被匹配到`news/index/type/top`。
静态路由定义不受URL后缀影响,例如: `http://serverName/Home/new/top.html` 也可以正常访问。
> 静态路由的路由地址 只支持字符串,格式:`[控制器/操作?]参数1=值1&参数2=值2`
# 闭包支持
## 闭包定义
我们可以使用闭包的方式定义一些特殊需求的路由,而不需要执行控制器的操作方法了,例如:
~~~
'URL_ROUTE_RULES'=>array(
'test' =>
function(){
echo 'just test';
},
'hello/:name' =>
function($name){
echo 'Hello,'.$name;
}
)
~~~
## 参数传递
闭包定义的参数传递在规则路由和正则路由的两种情况下有所区别。
##### 规则路由
规则路由的参数传递比较简单:
~~~
'hello/:name' =>
function($name){
echo 'Hello,'.$name;
}
~~~
规则路由中定义的动态变量的名称 就是闭包函数中的参数名称,不分次序。 因此,如果我们访问的URL地址是: `http://serverName/Home/hello/thinkphp`
则浏览器输出的结果是: `Hello,thinkphp`
如果多个参数可以使用:
~~~
'blog/:year/:month' =>
function($year,$month){
echo 'year='.$year.'&month='.$month;
}
~~~
##### 正则路由
如果是正则路由的话,闭包函数中的参数就以正则中出现的参数次序来传递,例如:
~~~
'/^new\/(\d{4})\/(\d{2})$/' =>
function($year,$month){
echo 'year='.$year.'&month='.$month;
}
~~~
如果我们访问: `http://serverName/Home/new/2013/03` 浏览器输出结果是: `year=2013&month=03`
## 继续执行
默认的情况下,使用闭包定义路由的话,一旦匹配到路由规则,执行完闭包方法之后,就会中止后续执行。如果希望闭包函数执行后,后续的程序继续执行,可以在闭包函数中使用布尔类型的返回值,例如:
~~~
'hello/:name' =>
function($name){
echo 'Hello,'.$name.'<br/>';
$_SERVER['PATH_INFO'] = 'blog/read/name/'.$name;
return false;
}
~~~
该路由定义中的闭包函数首先执行了一段输出代码,然后重新设置了`$_SERVER['PATH_INFO']`变量,交给后续的程序继续执行,因为返回值是false,所以会继续执行控制器和操作的检测,从而会执行Blog控制器的read操作方法。
假设blog控制器中的read操作方法代码如下:
~~~
public function read($name){
echo 'read,'.$name.'!<br/>';
}
~~~
如果我们访问的URL地址是: `http://serverName/Home/hello/thinkphp`
则浏览器输出的结果是:
~~~
Hello,thinkphp
read,thinkphp!
~~~
# 实例说明
我们已经了解了如何定义路由规则,下面我们来举个例子加深印象。
假设我们定义了News控制器如下(代码实现仅供参考):
~~~
namespace Home\Controller;
use Think\Controller;
class NewsController extends Controller{
public function read(){
$New = M('New');
if(isset($_GET['id'])) {
// 根据id查询结果
$data = $New->find($_GET['id']);
}elseif(isset($_GET['name'])){
// 根据name查询结果
$data = $New->getByName($_GET['name']);
}
$this->data = $data;
$this->display();
}
public function archive(){
$New = M('New');
$year = $_GET['year'];
$month = $_GET['month'];
$begin_time = strtotime($year . $month . "01");
$end_time = strtotime("+1 month", $begin_time);
$map['create_time'] = array(array('gt',$begin_time),array('lt',$end_time));
$map['status'] = 1;
$list = $New->where($map)->select();
$this->list = $list;
$this->display();
}
}
~~~
定义路由规则如下:
~~~
'URL_ROUTER_ON' => true, //开启路由
'URL_ROUTE_RULES' => array( //定义路由规则
'new/:id\d' => 'News/read',
'new/:name' => 'News/read',
'new/:year\d/:month\d' => 'News/archive',
),
~~~
然后,我们访问: `http://serverName/index.php/Home/new/8`
会匹配到第一个路由规则,实际执行的效果等效于访问: `http://serverName/index.php/Home/News/read/id/8`
当访问: `http://serverName/index.php/Home/new/hello`
会匹配到第二个路由规则,实际执行的效果等效于访问: `http://serverName/index.php/Home/News/read/name/hello`
那么如果访问: `http://serverName/index.php/Home/new/2012/03`
是否会匹配第三个路由规则呢?我们期望的实际执行的效果能够等效于访问: `http://serverName/index.php/Home/News/archive/year/2012/month/03`
事实上却没有,因为`http://serverName/index.php/Home/new/2012/`这个URL在进行路由匹配过程中已经优先匹配到了第一个路由规则了,把2012当成id的值传入了,这种情况属于路由规则的冲突,解决办法有两个:
**1、调整定义顺序**
路由定义改成:
~~~
'URL_ROUTE_RULES' => array( //定义路由规则
'new/:year\d/:month\d' => 'News/archive',
'new/:id\d' => 'News/read',
'new/:name' => 'News/read',
),
~~~
接下来,当我们再次访问: `http://serverName/index.php/Home/new/2012/03`
的时候,达到了预期的访问效果。所以如果存在可能规则冲突的情况,尽量把规则复杂的规则定义放到前面,确保最复杂的规则可以优先匹配到。但是如果路由规则定义多了之后,仍然很容易混淆,所以需要寻找更好的解决办法。
**2、利用完全匹配功能**
现在我们来利用路由的完全匹配定义功能,把路由定义改成:
~~~
'URL_ROUTE_RULES' => array( //定义路由规则
'new/:id\d$' => 'News/read',
'new/:name$' => 'News/read',
'new/:year\d/:month\d$' => 'News/archive',
),
~~~
在规则最后加上$符号之后,表示完整匹配当前的路由规则,就可以避免规则定义的冲突了。对于规则路由来说,简单的理解就是URL里面的参数数量或者类型约束要完全一致。 所以,如果我们访问 `http://serverName/index.php/Home/new/2012/03/01`
的话,是不会匹配成功任何一条路由的。
**3、利用正则路由**
当然,解决问题的办法总是不止一种,对于复杂的情况,我们不要忘了使用正则路由规则定义,在你找不到解决方案的时候,正则路由总能帮到你。 要实现上面的同样路由功能的话,还可以用下面的规则定义:
~~~
'URL_ROUTE_RULES' => array( //定义路由规则
'/^new\/(\d+)$/' => 'News/read?id=:1',
'/^new\/(\w+)$/' => 'News/read?name=:1',
'/^new\/(\d{4})\/(\d{2})$/' => 'News/achive?year=:1&month=:2',
),
~~~
- 序
- 前言
- 内容简介
- 目录
- 基础知识
- 起步
- 控制器
- 模型
- 模板
- 命名空间
- 进阶知识
- 路由
- 配置
- 缓存
- 权限
- 扩展
- 国际化
- 安全
- 单元测试
- 拿来主义
- 调试方法
- 调试的步骤
- 调试工具
- 显示trace信息
- 开启调试和关闭调试的区别
- netbeans+xdebug
- Socketlog
- PHP常见错误
- 小黄鸭调试法,每个程序员都要知道的
- 应用场景
- 第三方登录
- 图片处理
- 博客
- SAE
- REST实践
- Cli
- ajax分页
- barcode条形码
- excel
- 发邮件
- 汉字转全拼和首字母,支持带声调
- 中文分词
- 浏览器useragent解析
- freelog项目实战
- 需求分析
- 数据库设计
- 编码实践
- 前端实现
- rest接口
- 文章发布
- 文件上传
- 视频播放
- 音乐播放
- 图片幻灯片展示
- 注册和登录
- 个人资料更新
- 第三方登录的使用
- 后台
- 微信的开发
- 首页及个人主页
- 列表
- 归档
- 搜索
- 分页
- 总结经验
- 自我提升
- 进行小项目的锻炼
- 对现有轮子的重构和移植
- 写技术博客
- 制作视频教程
- 学习PHP的知识和新特性
- 和同行直接沟通、交流
- 学好英语,走向国际
- 如何参与
- 浏览官网和极思维还有看云
- 回答ThinkPHP新手的问题
- 尝试发现ThinkPHP的bug,告诉官方人员或者push request
- 开发能提高效率的ThinkPHP工具
- 尝试翻译官方文档
- 帮新手入门
- 创造基于ThinkPHP的产品,进行连带推广
- 展望未来
- OneThink
- ThinkPHP4
- 附录