ThinkPHP 6+ 中间件默认放在 app/middleware/ 目录下,需符合 PSR-4 命名规范;全局中间件在 app/middleware.php 中定义,路由中间件在路由绑定时注册,执行顺序为全局→路由→控制器。

ThinkPHP 中间件文件该放在哪个目录
默认情况下,ThinkPHP 6+ 的中间件类必须放在 app/middleware/ 目录下,且需符合 PSR-4 命名规范(类名与文件路径一致)。比如 app/middleware/CheckAuth.php 对应命名空间 app\middleware\CheckAuth。
这个路径不是硬编码死的,但改它需要同步调整 config/middleware.php 中的 middleware_namespace 配置,否则框架加载时会报 Class "xxx" not found。不建议随意挪动,尤其团队协作时——路径不统一会导致 composer dump-autoload 失效或本地能跑线上报错。
- 不要把中间件放进
app/common/或app/behavior/,后者是 TP5 的行为机制,TP6 已废弃 - 子目录可以有,例如
app/middleware/auth/LogLogin.php,对应类名app\middleware\auth\LogLogin,但需确保自动加载规则覆盖该子目录(默认 composer.json 已配置) - 如果用 Swoole 或 RoadRunner 长连接模式,中间件构造函数里别写阻塞操作(如 file_get_contents、curl),否则影响并发
全局中间件和路由中间件怎么注册才不冲突
全局中间件在 app/middleware.php 文件中通过数组定义,按顺序执行;路由中间件则在路由定义时显式绑定,优先级更高,且只对匹配的路由生效。两者共存时,执行顺序是:全局 → 路由 → 控制器方法(含注解中间件)。
容易踩的坑是误以为“后注册的中间件一定后执行”——其实全局中间件的顺序由数组索引决定,而路由中间件即使写在后面,也会插在全局之后、控制器之前。比如:
立即学习“PHP免费学习笔记(深入)”;
// route/app.php
Route::get('user/info', 'User/info')->middleware(['CheckAuth', 'LogRequest']);
这里 CheckAuth 和 LogRequest 会在所有全局中间件执行完后才运行,哪怕你在 middleware.php 末尾又加了一个 DebugTrace,它仍会比它们早执行。
- 全局中间件适合做请求入口统一处理:跨域头、日志记录、多语言初始化
- 路由中间件适合做业务隔离:后台路由加
AdminAuth,API 接口加ApiRateLimit,避免全局加载拖慢 H5 页面 - 同一个中间件类被同时注册为全局和路由时,会执行两次——除非你在中间件内部用静态变量或 Request 属性做幂等判断
中间件参数传递和闭包写法为什么很少用
ThinkPHP 支持以闭包形式定义中间件(直接写在路由或 middleware.php 里),例如:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
Route::get('test', function () { return 'ok'; })->middleware(function ($request, $next) { return $next($request); });
但这种写法无法复用、不可调试、不能被容器管理(比如依赖注入失效),而且 IDE 无法跳转、PHPStan 静态分析直接报错。官方文档虽没禁用,但实际项目中几乎没人用。
更常见的参数传递方式是「中间件类构造参数 + 路由绑定时传参」组合:
- 构造参数走容器绑定,在
app/provider.php里配app\middleware\RateLimit::class => [100, 60],适用于固定阈值 - 路由绑定时传参用字符串数组语法:
->middleware(['throttle:10,1']),框架会解析冒号后的值并传给中间件的handle方法第二个参数($params) - 注意:
throttle这种带参数的写法,要求中间件类必须实现think\contract\MiddlewareInterface,且handle方法签名得是public function handle($request, \Closure $next, ...$params)
中间件执行中断后响应体为啥有时是空的
最常见的原因是中间件里用了 return 但没返回 Response 实例。比如写了 return json(['code'=>401]),这在 TP6+ 里只是 PHP 数组,框架不会自动封装成 Response,最终响应体为空,状态码还是 200。
正确做法只有两种:
- 显式返回
Response对象:return response()->json(['code'=>401]); - 抛出异常并配好异常处理器(如
ValidateException自动转 JSON),靠think\ExceptionHandle统一兜底
另一个隐蔽问题是中间件里调了 exit 或 die ——这会绕过框架的响应发送逻辑,导致 Swoole 下连接卡住、Nginx 出现 502,且日志里完全没痕迹。务必用 response() + return 终止流程。
复杂点在于,有些中间件需要根据条件跳过后续流程但又不终止请求(比如灰度路由中间件),这时候不能 return,而是该用 $request->withAttribute('skip_auth', true) 传标记,让下游中间件自己判断是否跳过。这类控制流设计,比单纯“中断”更难测试,也更容易漏掉边界情况。


















