laravel中间件是洋葱模型而非链式调用,执行顺序为请求“进→出”双向;$next($request)代表剩余完整管道,必须且仅调用一次并处理其返回的响应。

中间件管道不是“链式调用”,而是闭包嵌套构造的洋葱模型;不理解 $next($request) 的执行时机,就一定会在自定义中间件里漏掉响应处理或提前终止失效。
中间件执行顺序为什么是「进→出」双向的
Laravel 的 Pipeline 并非简单地 foreach 依次调用中间件,而是用 array_reduce 从右往左把每个中间件的 handle() 包裹成一层闭包,最终形成类似 $middleware1($middleware2($middleware3($destination))) 的结构。
这意味着:
- 请求「进入」时:先执行最外层中间件(如
EncryptCookies),再逐层向内,直到$destination(即路由分发逻辑) - 响应「返回」时:从
$destination的返回值开始,逆序穿过每层中间件的handle()后半段(比如加 header、修改 response 内容) - 如果某个中间件没调用
$next($request),后续所有中间件(包括控制器)都不会执行,但也不会自动抛错——它只是静默截断
$next($request) 不是“继续往下走”,而是“触发整个剩余管道”
这是最容易误解的一点:$next 参数本身就是一个已组装好的闭包,它代表「当前中间件之后的所有处理流程(含控制器 + 其后中间件的响应阶段)」。
所以以下写法是错的:
public function handle($request, Closure $next)
{
// ❌ 错误:$next() 被当成普通函数调用,丢失了$request上下文
$next();
// ❌ 错误:两次调用,会导致控制器执行两次
$next($request);
return $next($request);
}
正确做法只调用一次,并接收其返回值(即完整响应):
public function handle($request, Closure $next)
{
// ✅ 请求阶段处理(如检查权限)
if (! auth()->check()) {
return redirect('/login');
}
// ✅ 唯一且必须的调用
$response = $next($request);
// ✅ 响应阶段处理(如添加header)
$response->headers->set('X-Processed-By', 'AuthMiddleware');
return $response;
}
自定义 Pipeline 复用中间件逻辑时要注意参数穿透
当你脱离 HTTP 上下文,在命令行或任务中手动使用 Illuminate\Pipeline\Pipeline 时,through() 传入的中间件类必须能接受任意类型的 $passable,且不能硬依赖 Request 或 Response。
常见陷阱:
- 中间件里直接调用
request()->xxx或response()->xxx→ 在非 HTTP 场景会报错 - 中间件构造函数依赖容器绑定(如注入
UserRepository),但没声明类型提示或未在app()->make()中显式解析 → 实例化失败 -
then()的$destination是个闭包,但它接收的参数必须和$passable类型一致;例如传入User对象,闭包签名就得是function(User $user)
示例(安全复用):
$result = app(Pipeline::class)
->send($user)
->through([EnsureUserIsActive::class, LogUserAction::class])
->then(function ($user) {
return $user->update(['last_seen_at' => now()]);
});
调试中间件执行流最有效的手段
不要靠 dd() 或日志猜顺序。真实项目中推荐两种方式:
- 在中间件
handle()开头加debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1),看调用栈里Pipeline::then如何层层嵌套 - 在
App\Http\Kernel.php的$middleware数组里临时插入一个「探针中间件」,用echo get_class($this) . PHP_EOL;输出执行路径(注意输出可能被缓冲,加flush()) - 避免在中间件里做耗时操作(如 DB 查询、HTTP 调用),因为它们会在每次请求「进+出」各执行一次 —— 容易引发重复写库、双倍 API 调用等隐蔽问题
真正难的从来不是写中间件,而是意识到:你写的每一行 handle() 方法,都在参与构建一个运行时动态生成的、不可见的闭包树。漏掉一次 $next(),或者错放一行响应处理代码,整棵树的某一分支就悄然失效了。











