ThinkPHP中间件执行顺序由config/middleware.php数组索引和链式调用次序决定:数组靠前的先执行请求、靠后的先完成响应;路由级中间件默认前置插入,$next($request)位置决定拦截或放行,启用middleware_trace可查看真实执行链。

ThinkPHP中间件执行顺序不是靠文件名、类名或注释判断的,而是由注册时的**数组索引顺序**和**链式调用次序**直接决定。你看到的执行流,就是代码里写出来的那个顺序。
看 config/middleware.php 数组索引
ThinkPHP 6.x 全局中间件只认 config/middleware.php(注意不是 app/middleware.php,后者已废弃)。这个文件返回一个数组,元素顺序 = 请求阶段执行顺序。
- 数组靠前的中间件,
handle()方法最先被调用(请求进) - 数组靠后的中间件,
handle()最先完成响应(响应出)——洋葱模型 - 比如:
ForceHttpsMiddleware必须放在第一位,否则后续中间件可能收到非 HTTPS 请求 - 若把
AuthMiddleware放在ParseJsonBody后面,$request->post()就是空的——因为 JSON body 还没被解析
路由级中间件默认前置插入,不是追加
在路由定义中用 ->middleware() 添加中间件,默认会插到当前路由中间件链的最前面,而不是末尾。
- 例如:全局注册了
[A, B],某路由又调用->middleware(C),实际执行顺序是C → A → B - 想强制追加到末尾?改用
->appendMiddleware(C) -
@middleware注解在 TP6+ 默认关闭,IDE 不识别、无法调试,生产环境禁用 - 敏感接口如
/api/v1/user/delete应显式叠加:->middleware([JwtAuth::class, CheckScope::class])
启用 middleware_trace 查看真实执行链
光看代码容易漏掉动态注册、条件加载或分组继承的中间件。最可靠的方式是让框架自己吐出来。
立即学习“PHP免费学习笔记(深入)”;
- 临时在入口或中间件中加:
ini_set('think.middleware_trace', '1'); - 发起请求后检查响应头:
X-Middleware-Stack字段,它会按实际执行顺序列出所有中间件类名 - 注意:该字段只在调试模式下生效,且需确保
app_debug = true - 如果字段为空,说明中间件根本没注册成功,或者被
->withoutMiddleware()显式排除了
$next($request) 的位置决定它是“拦”还是“兜”
中间件是否生效,不取决于它在数组里的位置,而取决于 handle() 里 $next($request) 是不是放在最后。
- 校验类中间件(鉴权、CSRF、IP 白名单)必须把
return $next($request);放在所有if判断之后 - 写成
$next($request); return response()->success();就等于放行后再返回成功——请求早已进入控制器,拦截失效 - 日志类中间件可以放后面,但它需要完整上下文(如
request()->controller()),太早调用会拿不到值 - 多语言中间件
LoadLangPack默认优先级是 10,如果你的日志切换或路由识别中间件没设更低优先级,$request->lang()就始终为空
真正容易被忽略的点是:中间件顺序一旦在应用启动时固化,就无法运行时重排;你以为改了配置文件就能生效,其实还可能被路由分组、控制器注解、或 appendMiddleware() 的缓存行为覆盖。调试时别只盯代码,先看 X-Middleware-Stack 头。



















