支柱页刚画完分拣台,本节把分拣规则手册一页页过完:路由有哪几种定义方式、参数怎么绑定、分组怎么组织、REST 风格怎么落地。读完本节,你应该能为「校园二手书」项目写出一张结构清晰的完整路由表,并且说得清每一行为什么这么写。
ThinkPHP 的路由写在 route 目录的文件里,常用的定义方式有三种。规则路由是主力,一行「地址到动作」的映射:
use think\facade\Route; // 规则路由:地址串 → 控制器动作 Route::get('book/:id', 'Book/detail'); // GET /book/42 Route::post('order', 'Order/create'); // POST /order Route::put('book/:id', 'Book/update'); // PUT /book/42 Route::delete('book/:id', 'Book/delete'); // 删除 // 闭包路由:直接返回响应,适合探活、跳转这类一次性小活 Route::get('health', function () { return json(['status' => 'ok']); }); // 注解路由:把规则写在控制器注释里,随控制器走 class Book extends BaseController { /** * @route("hot", method="GET") */ public function hot() { return json(BookService::hotList()); } }
三者的地盘划分很清楚:规则路由管业务主体——它集中、可审查、可统计;闭包路由只做框架级的零碎事(健康检查、临时跳转),业务逻辑写进闭包等于开历史倒车;注解路由胜在「规则跟代码走」,改控制器不用来回切换文件,团队约定一个使用边界即可(比如只允许用于单动作的简单路由)。没有哪种该独占,混用但守边界是常态。
⚠️ 常见坑:URL 访问模式是路由的兜底行为——即使不定义任何路由,
/控制器/动作也能访问。生产环境建议关闭这个兜底(强制走完整路由模式),否则路由表之外的暗门会一直存在。
地址里的 :id 是路由变量,默认匹配一段非斜杠内容。给变量加正则约束可以提前挡掉明显不合法的请求:
// 变量规则:id 必须是数字,year 是四位数字 Route::get('book/:id', 'Book/detail')->pattern(['id' => '\d+']); Route::get('archive/:year/:month', 'Book/archive') ->pattern(['year' => '\d{4}', 'month' => '\d{2}']); // 可选参数:不传时用默认值,动作方法里给同名默认参 Route::get('book/list/:page?', 'Book/list') ->pattern(['page' => '\d+']); class Book extends BaseController { public function list(int $page = 1) { return json(['page' => $page]); } }
参数在动作方法里按名字对号入座,配合容器的方法注入一起工作。这里要划一条与第 6 章验证器的分界线:路由参数约束只管「形状」(是不是数字、长度几位),不管「业务合法性」(这本书存不存在、你有没有权看)——形状过滤在分拣台完成,业务校验进站后再做,两层各司其职。
路由条目一多,平铺就会失控。分组把公共前缀、公共中间件、公共变量规则收拢到一起:
Route::group('book', function () { // 实际地址:/book/list、/book/:id、/book/:id/review Route::get('list', 'Book/list'); Route::get(':id', 'Book/detail')->pattern(['id' => '\d+']); Route::post(':id/review', 'Book/saveReview'); // 组内中间件:只作用于本组路由(回顾 2.4) })->middleware(\app\middleware\LoginAuth::class); // 多级分组:接口版本是典型场景 Route::group('api', function () { Route::group('v1', function () { Route::get('orders', 'api.v1.Order/index'); }); });
分组的价值不在少打几个字,而在「改一处、全组生效」:给整组接口加鉴权、换版本、限流,都只动分组那一行。路由表开始像目录树一样可读,是接口体系成熟的标志。
对标准的增删改查资源,框架提供资源路由一次注册全套 REST 风格规则:
// 一行注册,等效于七条规则 Route::resource('books', 'Book'); /* * GET /books index 列表 * GET /books/create create 新建表单页 * POST /books save 保存新建 * GET /books/:id read 详情 * GET /books/:id/edit edit 编辑表单页 * PUT /books/:id update 保存修改 * DELETE /books/:id delete 删除 */
对应的控制器提供同名方法即可。资源路由适合「围绕一张表的标准操作」,比如后台的商品管理;动作超出七种(比如「上架」「下架」)就单独补规则路由,不必硬塞进 REST 模子。判断标准看前端形态:页面驱动的管理后台用资源路由很顺;接口驱动的服务端,团队往往更愿意手写动词化的规则路由,两种取舍都成立,全组统一最重要。
视图与响应里指向站内地址时,用 URL 生成而不是手拼字符串:
use think\facade\Route; // 生成 /book/42 $url = (string) Route::buildUrl('Book/detail', ['id' => 42]); // 生成带查询串与锚点的地址 $url2 = (string) Route::buildUrl('Book/list') ->query(['page' => 2, 'kw' => 'php']) ->fragment('top');
手拼字符串的问题在路由改版那天爆发:/book/:id 改成 /books/:id,手拼的几十处地址全部失效且无编译期提示;用 URL 生成的项目里,改路由表生成结果自动跟着变。这个习惯和「视图里不写裸 SQL」同级,属于底线纪律。
背景:练习项目已有 Book、Order 两组控制器。操作:先纸上画 URL 清单——列表、详情、新建、下单、评论,各用什么方法与地址;再分两组写进路由文件,变量规则全部补上 pattern,下单组挂登录中间件;最后用命令行 php think route:list(或访问一个临时闭包路由打印路由表)核对。结果示例:GET /book/list、GET /book/:id、POST /order 三条主干加分组中间件,四十分钟内完成。解读:写完检查两件事——有没有动作方法漏了对应路由(靠强制完整路由模式暴露),有没有路由指向不存在的方法(访问即报错,顺手清理)。变式:给书店加「管理员删除任意书」能力,想想该新增路由级中间件还是控制器级,复习 2.4 的作用范围再动手。
地址表立好了,进站的请求怎么把「行李」安全交出来?下一节读输入:请求对象的规范用法。