ThinkPHP 6 的结构比 5.x 更接近现代框架:应用目录分离、容器与依赖注入更完整、路由文件独立。但从 5.x 迁过来的人常在「中间件到底注册在哪一层」上绕圈,这篇文章把这件事说清楚。

先看目录结构
app/:控制器、模型、中间件;route/app.php:路由定义;config/:数据库、缓存、Session 等配置;public/index.php:入口文件,只负责引导。
多应用模式下还有 app/admin/ 与 app/api/ 这样的独立应用目录,各自有控制器与路由文件,互不干扰。
路由的三种写法
use think\facade\Route;
// 闭包:只适合调试与极简接口
Route::get('ping', function () {
return json(['pong' => true]);
});
// 控制器方法:推荐
Route::get('posts/:id', 'Post/read')->pattern(['id' => '\d+']);
// 分组 + 中间件
Route::group('admin', function () {
Route::get('posts', 'admin.Post/index');
Route::post('posts', 'admin.Post/save');
})->middleware('adminAuth');
资源路由可以用 Route::resource('posts', 'Post'),它会一次生成七个动作。如果只需要部分动作,用 ->only([...]) 或 ->except([...]) 收窄,避免暴露不需要的接口。
三层中间件
中间件可以注册在三个位置,优先级从高到低:
- 全局:
app/middleware.php,所有请求都会经过; - 应用:多应用模式下
app/admin/middleware.php,只对该应用生效; - 路由:定义路由时
->middleware(...),最灵活。
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
class AdminAuth
{
public function handle(Request $request, Closure $next): Response
{
if (! session('admin_id')) {
return redirect('/admin/login');
}
return $next($request);
}
}
注册时可以用类名,也可以在 app/middleware.php 里起别名,路由中写别名更简洁。
执行顺序
请求按注册顺序进入,响应按相反顺序返回,和 Laravel 一样是洋葱模型。若一个中间件依赖 Session,必须排在 Session 初始化之后,否则 session() 永远取不到值。
参数绑定与验证
路由参数默认作为方法参数传入,类型会被自动转换。开启 route.php 里的严格模式后,参数类型不匹配会直接报错,比隐式转换更容易发现问题。
public function read(int $id)
{
$post = PostModel::findOrFail($id);
return view('post/read', ['post' => $post]);
}
表单验证推荐用验证器类而不是控制器里堆规则:
$validate = Validate::rule([
'title' => 'require|max:80',
'content' => 'require|min:20',
])->message([
'title.require' => '请填写标题',
]);
if (! $validate->check($data)) {
return json(['message' => $validate->getError()], 422);
}
几个常见坑
- 路由缓存后新增路由不生效:改完路由要重新生成缓存,开发环境直接关掉缓存;
- 控制器命名空间写错:
'admin.Post/index'对应app\admin\controller\Post,多一层目录就要多一层命名空间; - 中间件里直接
return一个字符串,框架会当成响应体,但状态码是 200,鉴权失败时应显式返回 401 / 403; - 跨域预检请求被自定义中间件拦下,导致 OPTIONS 请求 500。
小结
ThinkPHP 6 的路由与中间件机制并不复杂,真正容易出错的是「注册层级」与「执行顺序」。把鉴权、日志、跨域分别放到合适的层级,控制器就能保持轻薄。下一篇可以看模型关联与预载入,那是列表页性能的关键。