- 文档
- 开发工具
【免费下载链接】fig-standards
Standards either proposed or approved by the Framework Interop Group
导读
PSR-15(HTTP Server Request Handlers)是 PHP-FIG 通过的正式标准,为**服务端请求处理器(Request Handler)与HTTP 中间件(Middleware)**定义了统一接口,并规定二者基于 PSR-7 描述的 HTTP 消息工作。本文以仓库中的规范正文 accepted/PSR-15-request-handlers.md 为主体骨架,结合其配套元文档 accepted/PSR-15-request-handlers-meta.md 与 PSR-7、PSR-17 等相邻标准,完整讲解两个接口的定义、中间件设计流派(double pass 与 single pass/lambda)的取舍、设计决策理由,以及队列式与装饰式两套可运行的分发系统示例。读完本文,你将掌握如何在任意符合 PSR-15 的框架中编写可复用、可互操作的中间件与请求处理器,并理解其底层设计原理。
注:PSR-15 中所有对"请求处理器"与"中间件"的引用,均特指服务端(server request)请求处理场景,不涉及客户端/异步请求处理。
1. 为什么需要 PSR-15
在 PHP 生态中,PSR-7 定义了RequestInterface、ResponseInterface、ServerRequestInterface等 HTTP 消息抽象,但正如 PSR-15 规范开篇所述,HTTP 消息规范本身并不包含任何关于请求处理器或中间件的接口定义。而这两者却是任何 Web 应用的基础构件:
- 请求处理器:服务端代码接收请求消息、处理它并产生响应消息——几乎所有处理 HTTP 消息的代码都会包含某种请求处理器;
- 中间件:把通用的请求/响应处理逻辑从应用层抽离出来(如鉴权、日志、路由、跨域、压缩等),以组件形式参与请求处理链。
在没有统一标准之前,各框架各自定义了相似但签名略有出入的接口,导致中间件组件无法跨框架复用。PSR-15 通过形式化这两个接口,带来了如下收益(依据 meta 文档第 2 节):
- 为开发者提供一个可长期承诺的正式标准;
- 使任意中间件组件能在任何兼容框架中运行;
- 消除各框架对相似接口的重复定义;
- 避免方法签名上的细微差异。
规范正文明确了 PSR-15 的目标边界(meta 文档第 3 节):目标是创建基于 HTTP 消息的请求处理器接口与中间件接口,并确保二者与任何 HTTP 消息实现兼容;非目标包括:不规定 HTTP 响应的创建机制、不定义客户端/异步中间件接口、不规定中间件的分发方式(分发策略由各框架自行实现)。
2. 规范核心:请求处理器与中间件的职责
规范第 1 节给出了两个角色的精确定义,并分别给出 MUST 级约束。
2.1 请求处理器(Request Handlers)
一个请求处理器是独立的组件,它处理一个请求并产生一个响应(响应按 PSR-7 定义)。
- 请求处理器MAY在请求条件阻止其产生响应时抛出异常,异常类型不作定义;
- 符合本标准的请求处理器MUST实现
Psr\Http\Server\RequestHandlerInterface。
2.2 中间件(Middleware)
一个中间件组件是独立组件,通常与其它中间件组件协同,参与传入请求的处理与最终响应的生成(响应按 PSR-7 定义)。
- 当满足足够条件时,中间件MAY自行创建并返回响应,而无需委托给请求处理器;
- 符合本标准的中间件MUST实现
Psr\Http\Server\MiddlewareInterface。
2.3 生成响应(Generating Responses)
规范给出RECOMMENDED(强烈建议)级建议:任何需要生成响应的中间件或请求处理器,应当组合(compose)一个 PSR-7ResponseInterface原型,或一个能够生成ResponseInterface实例的工厂,以避免依赖某个特定的 HTTP 消息实现。这正是 PSR-17 HTTP 工厂标准出现的原因之一(详见下文第 7 节)。
2.4 处理异常(Handling Exceptions)
规范建议:任何使用中间件的应用都应包含一个捕获异常并将其转换为响应的组件。该中间件SHOULD作为最先执行的组件,并包裹住后续全部处理流程,从而保证无论发生什么情况都能生成响应。这一模式在实践中通常被称为"最外层错误处理中间件"。
3. 两个接口的完整定义
3.1Psr\Http\Server\RequestHandlerInterface
namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; /** * Handles a server request and produces a response. * * An HTTP request handler process an HTTP request in order to produce an * HTTP response. */ interface RequestHandlerInterface { /** * Handles a request and produces a response. * * May call other collaborating code to generate the response. */ public function handle(ServerRequestInterface $request): ResponseInterface; }要点:
- 只有一个方法
handle(),接收ServerRequestInterface,必须返回ResponseInterface; handle()内部可以调用其它协作代码来生成响应,接口本身不规定内部处理过程;- 请求处理器MAY将请求委托给另一个处理器(delegate),这为"中间件链"的实现留出了空间。
3.2Psr\Http\Server\MiddlewareInterface
namespace Psr\Http\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; /** * Participant in processing a server request and response. * * An HTTP middleware component participates in processing an HTTP message: * by acting on the request, generating the response, or forwarding the * request to a subsequent middleware and possibly acting on its response. */ interface MiddlewareInterface { /** * Process an incoming server request. * * Processes an incoming server request in order to produce a response. * If unable to produce the response itself, it may delegate to the provided * request handler to do so. */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface; }要点:
- 只有一个方法
process(),接收一个服务端请求与一个请求处理器,返回ResponseInterface; $handler参数是中间件委托产生响应的通道:中间件无法自行生成响应时,通过$handler->handle($request)将请求转交下游;- 接口文档注释明确指出中间件可以"作用于请求、生成响应、或将请求转发给后续中间件并可能作用于其响应"。
4. 中间件设计流派:double pass 与 single pass(lambda)
meta 文档第 5 节详细记录了 PSR-15 在制定过程中调研的两大中间件设计流派,这是理解"为什么接口长这样"的关键背景。
4.1 Double Pass(双传模式)
大多数既有中间件实现采用这种签名,其原型来自 Express 中间件:
fn(request, response, next): response即调用中间件时传入 3 个参数:
- 一个
ServerRequestInterface实现; - 一个
ResponseInterface实现; - 一个接收请求与响应的
callable,用于委托给下一个中间件。
由于请求和响应都被传入中间件,故称 "double pass"。meta 文档列举了使用该模式的框架(如 mindplay/middleman、relay/relay v1、Slim v3、Zend Stratigility v1)以及大量基于该模式编写的中间件包。该模式的显著缺点是:接口本身是可调用类型(callable),目前没有任何方式对闭包做严格类型约束,callable类型提示无法保证传入的可调用对象确实实现了中间件签名,降低了运行时安全性。
4.2 Single Pass / Lambda(单传模式)
另一种流派更接近 StackPHP 风格:
fn(request, next): response其共性为:
- 中间件通过一个具体接口定义,接口内有一个接收请求进行处理的方法;
- 调用时只传入 2 个参数:① 一个 HTTP 请求消息;② 一个可供中间件委托生成响应的请求处理器。
在该形式下,中间件在请求处理器生成响应之前无法接触到响应;响应生成后,中间件仍可在返回前修改它。由于只有请求被传入中间件,故称 "single pass" 或 "lambda"。
meta 文档还指出,Guzzle 的中间件(面向出站客户端请求)使用了function (RequestInterface $request, array $options): ResponseInterface签名;StackPHP(基于 Symfony HttpKernel)与 Laravel 中间件(handle(Request $request, callable $next): Response)也属于此流派的变体——它们的共同点是响应对象不包含在传入参数中。
4.3 为什么最终选择 lambda 单传
meta 文档第 5.4 节明确指出:尽管 double pass 在 PSR-7 早期采用者中几乎被普遍使用,但它存在严重实现问题:
- 传入空响应无法保证其处于可用状态,且中间件可能在继续传递前就修改了响应;
- 无法确保响应体尚未被写入——这可能导致输出不完整,或错误响应附带缓存头被发出;在已有响应体上覆盖写入时,若新内容比原内容短,还可能产生损坏的响应体内容;最有效的解决方式是修改消息体时始终提供全新的流(fresh stream);
- 依赖倒置并非只能靠传入响应实现:虽然传入响应有助于避免依赖特定 HTTP 消息实现,但同样可以通过向中间件注入创建 HTTP 消息对象的工厂(或注入空消息实例)来解决——PSR-17 HTTP 工厂出现后,这种标准化的依赖倒置方案已然可行;
- double pass 中间件普遍使用
callable类型提示,导致严格类型(strict typing)无法落地,运行时安全下降。
由于这些显著问题,提案最终选择了 lambda(单传)方案——即本文第 3 节展示的接口形态:中间件只接收请求与请求处理器,由请求处理器负责产生响应。
5. 接口设计决策的深层考量
meta 文档第 6 节逐条回答了接口设计中的关键问题,这些理由对理解规范意图至关重要。
5.1 为什么用handle()而非__invoke()
- 使用命名方法比
__invoke更透明; - 当请求处理器被赋值给类变量时,命名方法调用更自然,无需借助
call_user_func等不常见语法。 - (旧版规范曾用 "delegate" 一词,但该术语暗示了内部行为;
handle只规定"最终产生响应",不规定内部行为,更为宽松。)
5.2 为什么中间件方法命名为process()
工作组调研了既有框架常用的方法名,发现普遍存在以下三种:
__invoke(Slim、Expressive、Relay 等中间件系统);handle(尤其源自 Symfony HttpKernel 的软件);dispatch(Zend Framework 的 DispatchableInterface)。
为了给这些既有类前向兼容地转型为本规范兼容的中间件留出空间,必须选择一个不常用的名字,于是选择了process,以表达"处理请求"之意。同理,中间件接口不使用__invoke,是为了避免与已实现 double pass 且希望为兼容本规范而同时实现该接口的既有中间件发生冲突。
5.3 为什么强制要求服务端请求(ServerRequestInterface)
- 对请求处理器与中间件都要求
ServerRequestInterface,是为了明确它们只能用于同步的服务端上下文; - 客户端场景下,出站请求通常异步处理、可并行发起,往往会返回一个响应 promise(而非同步响应),这不在本规范范围内;过早定义客户端中间件被视为不成熟之举,未来应由专门的提案针对异步中间件特性制定标准。
5.4 请求处理器在中间件系统中的角色
meta 文档第 6.2 节归纳了中间件的三种职责,并给出对应的典型代码:
- 自行产生响应:满足特定请求条件时,中间件直接生成并返回响应;
- 返回请求处理器的结果:中间件无法自行产生响应时,委托请求处理器生成——有时会传入经过变换的请求(例如注入请求属性,或传入解析请求体的结果);
- 操纵并返回处理器产生的响应:例如对响应体做 gzip 压缩、添加 CORS 头等——此时中间件捕获处理器返回的响应,变换后再返回。
// 直接委托: return $handler->handle($request); // 捕获响应以便操纵: $response = $handler->handle($request);典型的分发场景有两种:
- 队列/栈式处理器:处理器内部维护中间件队列,调用
$handler->handle($request)会推进内部指针、取出对应中间件并以$middleware->process($request, $this)调用它;队列耗尽时通常抛出异常或返回预设响应; - 路由中间件:匹配传入请求到具体处理器,并返回该处理器生成的响应;若无法路由,则执行传入中间件的处理器。
6. 接口协作实战:两套可运行的中间件分发系统
meta 文档第 6.3 节给出了工作组观察/实现的两套分发系统完整示例。注意:它们不是规范钦定的唯一方案,而是展示RequestHandlerInterface与MiddlewareInterface如何协同工作的可运行参考实现。
6.1 队列式请求处理器(Queue-based request handler)
该方案中,请求处理器维护一个中间件队列,并持有一个"兜底响应"——当队列耗尽仍未产生响应时返回之。执行第一个中间件时,队列将自身作为请求处理器传给中间件:
class QueueRequestHandler implements RequestHandlerInterface { private $middleware = []; private $fallbackHandler; public function __construct(RequestHandlerInterface $fallbackHandler) { $this->fallbackHandler = $fallbackHandler; } public function add(MiddlewareInterface $middleware) { $this->middleware[] = $middleware; } public function handle(ServerRequestInterface $request): ResponseInterface { // 队列中最后一个中间件已调用过请求处理器。 if (0 === count($this->middleware)) { return $this->fallbackHandler->handle($request); } $middleware = array_shift($this->middleware); return $middleware->process($request, $this); } }应用引导(bootstrap)示例:
// 兜底处理器: $fallbackHandler = new NotFoundHandler(); // 创建请求处理器实例: $app = new QueueRequestHandler($fallbackHandler); // 添加一个或多个中间件: $app->add(new AuthorizationMiddleware()); $app->add(new RoutingMiddleware()); // 执行: $response = $app->handle(ServerRequestFactory::fromGlobals());注意:ServerRequestFactory::fromGlobals()在此是示意性的全局请求工厂调用(真实实现取决于具体 PSR-7 库,例如可借助 PSR-17 的ServerRequestFactoryInterface来创建)。
该系统的优势(meta 文档归纳):
- 中间件无需知晓其它中间件或它们在应用中的组合方式;
QueueRequestHandler对所用 PSR-7 实现完全无感;- 中间件按加入顺序执行,代码意图明确;
- "兜底响应"的生成委托给应用开发者,由开发者决定返回 404、默认页还是其它内容。
6.2 装饰式请求处理器(Decoration-based request handler)
该方案中,请求处理器同时装饰一个中间件实例和一个兜底请求处理器。应用从内向外构建,把每一层请求处理器传给其外层:
class DecoratingRequestHandler 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); } } // 创建一个响应原型,用于没有任何中间件能自行产生响应时返回。 // 它可以是 404、500 或默认页面。 $responsePrototype = (new Response())->withStatus(404); $innerHandler = new class ($responsePrototype) implements RequestHandlerInterface { private $responsePrototype; public function __construct(ResponseInterface $responsePrototype) { $this->responsePrototype = $responsePrototype; } public function handle(ServerRequestInterface $request): ResponseInterface { return $this->responsePrototype; } }; $layer1 = new DecoratingRequestHandler(new RoutingMiddleware(), $innerHandler); $layer2 = new DecoratingRequestHandler(new AuthorizationMiddleware(), $layer1); $response = $layer2->handle(ServerRequestFactory::fromGlobals());与队列式类似,这里的请求处理器同样承担两个职责:产生兜底响应(当没有任何一层产生响应时)与分发中间件。示例中的匿名类$innerHandler是"响应原型"(response prototype)模式的典型体现——它返回一个预设状态的 404 响应,避免了对特定响应实现的直接依赖。
6.3 可复用中间件的编写指南
meta 文档总结出三条最大化互操作性的编写准则:
- 测试请求是否满足前置条件:若不满足,使用组合的原型响应或响应工厂生成并返回响应;
- 前置条件满足时,委托请求处理器生成响应,可传入变换后的"新请求",例如
$handler->handle($request->withAttribute('foo', 'bar'))(withAttribute由 PSR-7 的ServerRequestInterface定义,见 accepted/PSR-7-http-message.md,用于注入路由匹配结果等派生属性); - 要么原样传递处理器返回的响应,要么操纵后返回新响应,例如
return $response->withHeader('X-Foo-Bar', 'baz')。
6.4 两个综合示例中间件
AuthorizationMiddleware完整覆盖了上述三条准则:
class AuthorizationMiddleware implements MiddlewareInterface { private $authorizationMap; public function __construct(AuthorizationMap $authorizationMap) { $this->authorizationMap = $authorizationMap; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (! $this->authorizationMap->needsAuthorization($request)) { return $handler->handle($request); } if (! $this->authorizationMap->isAuthorized($request)) { return $this->authorizationMap->prepareUnauthorizedResponse(); } $response = $handler->handle($request); return $this->authorizationMap->signResponse($response, $request); } }其行为逻辑:
- 若无需鉴权 →直接委托,请求原样交给处理器;
- 若需要鉴权但未授权 → 用组合好的响应生成"未授权"响应;
- 若需要鉴权且已授权 → 委托处理器,并根据请求对返回的响应签名(sign)。
关键点:中间件完全不关心请求处理器如何实现,它只是在前置条件满足时利用处理器产生响应。
RoutingMiddleware遵循类似流程:先分析请求是否匹配已知路由;在本实现中路由映射到请求处理器,中间件本质上委托它们产生响应;若未匹配到任何路由,则执行传入的处理器来产生响应:
class RoutingMiddleware implements MiddlewareInterface { private $router; public function __construct(Router $router) { $this->router = $router; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $result = $this->router->match($request); if ($result->isSuccess()) { return $result->getHandler()->handle($request); } return $handler->handle($request); } }这两段示例与第 6.1、6.2 节的分发系统相互印证:同一组中间件代码无需任何修改,即可同时运行在队列式和装饰式两种架构中,这正是"松耦合可复用中间件"的设计目标。
7. 与 PSR-7、PSR-17 的协同关系
7.1 基于 PSR-7 的消息模型
PSR-15 的两个接口的方法签名直接使用 PSR-7 的ServerRequestInterface与ResponseInterface(定义见 accepted/PSR-7-http-message.md)。其中ServerRequestInterface扩展了RequestInterface,封装了 PHP 超全局数据($_SERVER、$_COOKIE、$_GET、$_POST、$_FILES等),并额外提供attributes(请求属性)机制:getAttributes()、getAttribute($name, $default)、withAttribute($name, $value)、withoutAttribute($name)允许应用在请求处理链中注入派生数据(如路由匹配结果、解密后的 Cookie、反序列化的请求体),并实现多个请求消费者之间的消息传递。这在第 6.3 节$handler->handle($request->withAttribute('foo', 'bar'))的用法中得到了直接体现。
7.2 借助 PSR-17 实现依赖倒置
规范 1.3 节"生成响应"建议中间件/请求处理器组合响应原型或响应工厂。PSR-17(accepted/PSR-17-http-factory.md)正是为此提供的标准接口集合,其中与本主题最相关的是:
ResponseFactoryInterface::createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface——生成响应实例;ServerRequestFactoryInterface::createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface——生成服务端请求;StreamFactoryInterface——创建新流,规避 double pass 模式下响应体已被写入而无法恢复的问题。
meta 文档特别指出:double pass 通过"传入响应"实现依赖倒置的论点,在 PSR-17 出现后可以由注入工厂这一更标准的方式替代——中间件组合一个ResponseFactoryInterface即可在任何 PSR-7 实现上创建响应,同时保持对具体实现的完全无感。
7.3 完整的技术闭环
由此形成完整的标准链条:PSR-7 定义消息模型 → PSR-17 定义对象创建工厂 → PSR-15 定义请求处理与中间件流程。请求处理器是链条的终端执行者(接收请求、产生响应),中间件是链条中的可插拔环节(变换请求、短路响应、变换响应),而分发系统(队列式或装饰式)把中间件串接起来并最终委托给应用代码。这份闭环逻辑在本仓库中均有对应的正式文档可供查阅:PSR-15-request-handlers.md(规范正文)、PSR-15-request-handlers-meta.md(设计决策与示例)、PSR-7-http-message.md 与 PSR-17-http-factory.md。
8. 结语:面向框架与中间件作者的行动清单
如果你要实现中间件:
- 实现
MiddlewareInterface::process(),把"自行产生响应""委托给处理器""操纵处理器响应"三种路径分开处理; - 需要创建响应时,组合响应原型或
ResponseFactoryInterface工厂,避免依赖具体 PSR-7 实现; - 需要传参给下游时,用
$request->withAttribute(...)产生新请求,而非修改原请求。
如果你要构建应用/框架:
- 实现
RequestHandlerInterface,并选择队列式或装饰式分发策略(或框架自定义的其它策略); - 提供兜底请求处理器(404/500/默认页),并在中间件链最外层放置"捕获异常转响应"的中间件(规范 1.4 节建议);
- 严格遵循接口签名:
handle(ServerRequestInterface $request): ResponseInterface与process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface。
遵循以上约定,你的中间件与请求处理器即可在任何符合 PSR-15 的框架间无缝移植。
- 文档
- 开发工具
【免费下载链接】fig-standards
Standards either proposed or approved by the Framework Interop Group
相关推荐
ShowDoc 中的 PSR-15 请求处理器:解析 psr/http-server-handler 接口规范与中间件协作机制
ShowDoc 中的 PSR 15 请求处理器:解析 psr/http server handler 接口规范与中间件协作机制 导读 本文聚焦 ShowDoc
文档知识库后端前端Wasp 自定义注册 Action 完全指南:接管 signup 全流程与 Validators API 实战
Wasp 自定义注册 Action 完全指南:接管 signup 全流程与 Validators API 实战 本篇技术指南基于 Wasp 0.18 版本文档,
文档开发工具如何3分钟获取阿里云盘Refresh Token:扫码实现自动化文件管理的终极指南
如何3分钟获取阿里云盘Refresh Token:扫码实现自动化文件管理的终极指南 阿里云盘Refresh Token获取工具是一款让普通用户也能轻松掌握云盘自
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考