You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PSR-15规范下如何通过中间件处理并修改响应?

理解PSR-15中间件的洋葱模型:不用拆分入站/出站栈

我刚看到你的问题,其实你困惑的核心是没搞明白PSR-15的中间件本身就内置了处理响应的能力,完全不需要把入站和出站逻辑拆成两个独立的栈。咱们一步步理清楚:

先纠正一个误解:PSR-15中间件不是只能处理请求

你提到的MiddlewareInterface::process()方法确实接收RequestInterface并返回ResponseInterface,但关键是它还接收第二个参数RequestHandlerInterface——这个就是洋葱的"下一层":要么是下一个中间件,要么是最终的请求处理器(比如你的控制器)。

当你的中间件调用$handler->handle($request)时,它会等待下一层执行完成并返回响应,这时候你就可以在中间件里修改这个响应,再返回给上一层。这就完美实现了洋葱模型的双向传递:请求穿过中间件到核心处理器,响应再从核心处理器穿回中间件。

举个实际例子:一个处理响应压缩的PSR-15中间件

比如我们写一个Gzip压缩中间件,它既可以在入站时检查请求头,又可以在出站时压缩响应:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use GuzzleHttp\Psr7\Stream;

class GzipMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        // 1. 入站逻辑:检查客户端是否支持gzip压缩
        $acceptEncoding = $request->getHeaderLine('Accept-Encoding');
        $supportsGzip = str_contains($acceptEncoding, 'gzip');

        // 2. 传递请求到下一层(可能是另一个中间件,也可能是控制器)
        $response = $handler->handle($request);

        // 3. 出站逻辑:如果支持gzip,就压缩响应内容
        if ($supportsGzip && !$response->hasHeader('Content-Encoding')) {
            $compressedBody = gzencode((string) $response->getBody(), 9);
            
            $response = $response
                ->withHeader('Content-Encoding', 'gzip')
                ->withHeader('Content-Length', strlen($compressedBody))
                ->withBody(new Stream(fopen('php://temp', 'rwb+')));
            
            $response->getBody()->write($compressedBody);
            $response->getBody()->rewind();
        }

        return $response;
    }
}

看,这个中间件同时处理了入站(检查请求头)和出站(压缩响应)逻辑,完全符合PSR-15规范,也完美适配洋葱模型。

你的代码该怎么修改?合并中间件栈,按洋葱顺序排列

你现在拆分了入站和出站栈,虽然能跑,但不符合PSR-15的设计,也浪费了洋葱模型的灵活性。正确的做法是把所有中间件(不管是处理请求还是响应的)放到同一个栈里,按照洋葱顺序排列:

  • 入站时需要先执行的中间件,放在栈的前面(比如CSRF检查、请求解析)
  • 出站时需要后执行的中间件,也放在栈的前面(比如响应压缩,因为它要在所有响应处理的最后一步执行)

举个例子,你的中间件栈应该是这样的:

$all_middlewares = [
    new Alpha,    // 入站:CSRF检查;出站:添加安全响应头
    new Bravo,    // 入站:验证请求参数;出站:格式化错误响应
    new Charlie,  // 入站:解析JSON请求体;出站:统一响应格式
    new Delta,    // 入站:无;出站:压缩HTML内容
    new Foxtrot,  // 入站:无;出站:添加缓存控制头
    new Golf,     // 入站:无;出站:记录响应日志
];

然后你需要把这些中间件和最终的请求处理器(路由匹配后的控制器)组装成一个完整的中间件栈。这里可以自己实现一个简单的栈逻辑,或者用现成的库:

// 创建最终的请求处理器:路由匹配后调用控制器
$finalHandler = new class($router) implements RequestHandlerInterface {
    private $router;

    public function __construct($router)
    {
        $this->router = $router;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        [$controller, $method] = $this->router->match($request);
        return (new $controller)->$method($request);
    }
};

// 构建中间件栈:从最内层开始包裹,所以要倒序遍历中间件
$middlewareStack = $finalHandler;
foreach (array_reverse($all_middlewares) as $middleware) {
    $middlewareStack = new class($middleware, $middlewareStack) implements RequestHandlerInterface {
        private $middleware;
        private $nextHandler;

        public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $nextHandler)
        {
            $this->middleware = $middleware;
            $this->nextHandler = $nextHandler;
        }

        public function handle(ServerRequestInterface $request): ResponseInterface
        {
            return $this->middleware->process($request, $this->nextHandler);
        }
    };
}

// 处理请求,得到最终响应
$serverResponse = $middlewareStack->handle($request);

这样组装后,整个流程就是标准的洋葱模型:

  1. 请求依次经过Alpha → Bravo → Charlie → Delta → Foxtrot → Golf的入站逻辑
  2. 到达最终处理器(控制器),生成响应
  3. 响应依次经过Golf → Foxtrot → Delta → Charlie → Bravo → Alpha的出站逻辑
  4. 最后返回给客户端

为什么Laravel看起来不一样?

Laravel的中间件系统虽然不是严格遵循PSR-15,但本质逻辑是一样的。它的中间件handle方法接收一个Closure $next,这个闭包就相当于PSR-15的RequestHandlerInterface:

public function handle($request, Closure $next)
{
    // 入站逻辑:比如检查权限
    if (!auth()->check()) {
        return redirect('/login');
    }

    // 传递请求到下一层,拿到响应
    $response = $next($request);

    // 出站逻辑:比如添加响应头
    $response->header('X-Frame-Options', 'SAMEORIGIN');

    return $response;
}

你看,和PSR-15的中间件逻辑完全一致,只是语法不同而已。Laravel底层也是把这些中间件组装成一个洋葱栈,只是封装得比较深,你看不到底层的RequestHandlerInterface而已。

总结

你的当前实现虽然能运行,但不是PSR-15推荐的方式。正确的做法是让每个中间件同时处理入站请求和出站响应,把所有中间件放到同一个栈里,按照洋葱顺序排列。这样既符合规范,又能充分利用洋葱模型的灵活性——比如你可以在同一个中间件里同时处理请求的验证和响应的格式化,逻辑更集中。

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 19:32:39