PSR 15 HTTP 请求处理 - 说明文档
1. 概要
本文档定义了用于处理 HTTP 消息的 HTTP 请求处理程序 ( 「请求处理程序」 ) 和 HTTP 中间件组件 (「中间件」 ) 的通用接口
这里的 HTTP 消息必须遵许 PSR 7 或后续替换 PSR 规范
注意: 本规范提及的「请求处理程序」和 「中间件」 限定于处理 HTTP 服务请求
2. 何必 ?
HTTP 消息规范 ( PSR 7 ) 没有介绍请求处理程序或中间件
请求处理程序是 Web 应用程序的基本组成部分。处理程序是一个接收请求并生成响应的组件。 几乎所有遵循 PSR 7 HTTP 消息规范的代码都会有某种请求处理程序
Middleware 在 PHP 生态系统中已经存在了许多年。可重用中间件的基本概念也因为 StackPHP 而流行起来。
自从 HTTP 消息规范发布以来,越来越多的框架都采用了遵循 HTTP 消息接口的中间件
遵循统一规范的请求处理器和中间件可以消除很多问题,并且具有许多的优点:
- 为开发者提供了一个正式的标准
- 使任何中间件组件都能在任何兼容的框架中运行
- 消除各种框架中重复定义的接口
- 消除方法签名中的小差异
3. 内容
3.1 目标
- 创建一个符合 HTTP 消息 ( PSR 7 ) 规范的请求处理器接口
- 创建一个符合 HTTP 消息 ( PSR 7 ) 规范的中间件接口
- 根据最佳实战原则实现处理器和中间件
- 确保请求处理器与中间件和 HTTP 消息规范的任何实现相兼容
3.2 非目标
- 尝试定义一种机制用于选择哪种 HTTP 响应该被创建
- 尝试定义客户端/异步中间件
- 尝试定义中间件如何被调度
4. 请求处理器方法
有多种方式可以创建一个符合 HTTP 消息 ( PSR 7 ) 规范的请求处理器。但它们都有一个相同的处理过程:
接受一个 HTTP 请求,并生成相对应的 HTTP 响应
该提案不会确定如何实现一个请求处理器的内部逻辑,处理器的内部实现可以框架和应用而各不相同
5. 中间件方法
目前有两种方式可以创建一个符合 HTTP 消息 ( PSR 7 ) 规范的中间件
5.1 Double Pass
大多数中间件实现使用的函数签名是相同的,都是基于 Express middleware 中间件
这种中间件的函数签名如下
fn(request, response, next): response
框架中已经实现的中间件几乎都采用了这个签名
不难发现他们都有如下共同点:
- 中间件被定义为是可调用的 ( callable )
-
调用中间件需要传递三个参数:
- 一个
ServerRequestInterface
接口实现 - 一个
ResponseInterface
接口实现 - 一个可调用 (
callable
) 的且能够接收请求并响应委派给下一个中间件的函数
- 一个
大量的项目提供和/或使用完全相同的接口。这种同时传递请求和响应给中间件的方法通常被称为 「 双重传递 ( double-pass ) 」
5.1.1 使用双重传递的项目
5.1.2 实现了双重传递的中间件
- bitexpert/adroit
- akrabat/rka-ip-address-middleware
- akrabat/rka-scheme-and-host-detection-middleware
- bear/middleware
- los/api-problem
- los/los-rate-limit
- monii/monii-action-handler-psr7-middleware
- monii/monii-nikic-fast-route-psr7-middleware
- monii/monii-response-assertion-psr7-middleware
- mtymek/blast-base-url
- ocramius/psr7-session
- oscarotero/psr7-middlewares
- php-middleware/block-robots
- php-middleware/http-authentication
- php-middleware/log-http-messages
- php-middleware/maintenance
- php-middleware/phpdebugbar
- php-middleware/request-id
- relay/middleware
这种双重传递的主要缺点是:虽然接口本身是可调用的,但目前没有办法严格传递一个闭包
5.2 单传递 ( Lambda )
中间件采用的另一种方式则更接近 StackPHP 风格,一般有如下签名
fn(request, next): response
采用这种方式的中间件一般都具有以下共同点:
-
中间件定义了一个特定请求方法的接口用于处理请求
-
调用中间件需要传递两个参数:
- 一个 HTTP 请求消息
- 一个请求处理器,中间件可以委托这个请求处理器生成 HTTP 响应消息
这种方式中,中间件只有在请求处理器生成了一个响应后才能访问响应的内容。中间件可以在返回响应内容之前修改它
这种只传递请求给中间件的方法通常被称为 「 单传递 ( single pass ) 」 或 「 表达式 ( lambda ) 」
5.2.1 使用单传递的项目
符合 HTTP 消息规范的项目中,采用这种方式的例子极少,但有一个格外耀眼
Guzzle middleware 是一个在客户端使用的中间件,它使用了下面的函数签名
function (RequestInterface $request, array $options): ResponseInterface
5.2.2 使用单传递的其它项目
还有一些其它的重要项目会使用这种方式预先处理 HTTP 消息
StackPHP 项目基于 Symfony HttpKernel 支持下面这种函数签名的中间件
function handle(Request $request, $type, $catch): Response
注意: 虽然 Stack 可以接受多个参数,但响应对象没有被包含在其中
Laravel middleware 使用了 Symfony 组件并支持下面这种函数签名的中间件
function handle(Request $request, callable $next): Response
5.3 方式比较
PHP 社区使用单传递中间件已经有很多年了,大多数基于 StackPHP 的软件包是最明显的例子
虽然双重传递方式更新,但已经大量被使用在早期的 HTTP 消息 ( PSR 7 ) 实现中
5.4 决策思路
尽管几乎普遍采用双通方法,但在执行方面存在重大问题
最严重的是: 传递一个空的响应并不能保证响应处于可用状态。中间件在将其传递给下一步处理之前可能会修改响应,这进一步加剧了这一点
更进一步的问题是,目前没有办法去检查响应主体没有被写入,这可能导致不完整的输出或附加了缓存头的错误响应
如果新内容短于原始内容,则在写入现有主体内容时,也可能会损坏主体内容 ( corrupted body content ) 。 解决这些问题的最有效方法是在修改消息正文时始终提供新的流
有些人争论说通过传递响应内容有助于确保依赖倒置原则。虽然确实有助于避免依赖于 HTTP 消息的特定实现,但这个问题可以通过将工厂方法注入中间件来创建 HTTP 消息对象或注入空的实例来解决
随着 PSR 17 标准规范中 HTTP 工厂方法的的确立,处理依赖倒置的标准方法成为了可能
尽管确实有助于避免依赖于HTTP消息的特定实现,但也可以通过将工厂注入中间件来创建HTTP消息对象或注入空消息实例来解决问题。随着PSR-17中HTTP工厂的创建,处理依赖性反转的标准方法成为可能。
一个更主观但也很重要的问题是,现有的双向访问中间件通常使用 callable
的类型提示来调用中间件。这使得严格的类型限制变得不可能,因为不能保证传递的可 callable
的中间件实现了中间件接口里的方法,这会降低了运行时安全性
由于这些重大问题悬而未决,本提案最终选择了 lambda 方法
6. 设计决策
6.1 请求处理器设计
RequestHandlerInterface
接口只定义了一个方法,用于接收一个请求且 必须 返回一个响应
当然了,请求处理器 可以 不直接返回响应而是委托另一个处理器来返回响应
为什么需要服务端请求参数 ?
是为明确说明中间件只能用在同步的服务端上下文中
在客户端上下文中,通常会返回一个 promise 而不是一个响应
为什么是术语「 handler (处理器) 」?
术语「 handler (处理器) 」的原义是被指定为用于管理和控制的东西
就 请求处理 而言,一个请求处理器就是用来处理请求以产生响应的关键点
与本规范先前版本中使用的术语 「委托 ( delegate ) 」 不同,并没有指定此接口的内部行为。只要请求处理器最终能生成响应,它就是有效的
为什么请求处理器不使用魔术方法 __invoke
?
-
使用其它的方法名比使用
__invoke
方法语义更清晰 -
当把请求处理器赋值给一个类变量时,也能容易的被调用,因为不需要使用
call_user_func
或者其它不常见的语法很多人难以理解这一点,首先 PHP 的类型约束对 __invoke() 方法不管用,也就是说一个类有没有实现 __invoke() 方法是未知的
6.2 中间件设计
MiddlewareInterface
接口只有一个方法,接收 HTTP 请求和 HTTP 请求处理器作为参数,并返回一个响应
中间件可以
- 将请求传递给请求处理器前修改请求
- 返回请求处理器的结果前修改响应
- 创建并返回响应,而不用将请求传递给请求处理器,也就是说中间件自己处理了请求
当按顺序从一个中间件委派给另一个中间件时,调度系统的一种实现思路是使用一个持有中间件列表的请求处理器来承担中介的角色将所有中间件链接在一起
列表中最终或最内层的中间件将扮演应用程序入口的角色,根据得到的结果生成一个响应,或者 可以 将生成响应的任务委派给专门的请求处理器
为什么中间件不使用魔术方法 __invoke
?
如果中间件使用 __invoke
方法,那么它将会和实现了相互访问的现有中间件发生冲突,已经存在的中间件就需要重新编写 __invoke()
以达到和此规范的前向兼容性
为什么命名为 process()
?
我们调查了大量的现有的中间件和框架,以确定每个处理器传入请求的方法。我们发现以下几个方法是最常用的
__invoke
( 出现在一些中间件中,比如 Slim, Expressive, Relay 等等 )handle
( 出现在 Symfony 开发的 HttpKernel)dispatch
( 出现在 Zend Framework 的 DispatchableInterface)
为了让这些已经存在的类库能够继续作为中间件向前保持兼容性,我们需要选择一个不常用的名称,最终选择了 process
来指示 处理 一个请求
我们选择允许这种类的前向兼容方法将其本身重新定位为中间件(或与此规范兼容的中间件),因此需要选择一个不常用的名称。 因此,我们选择流程来指示处理请求
为什么需要一个 服务端请求 参数?
是为明确说明中间件只能用在同步的服务端上下文中
但并不是所有中间件都会用到服务端请求接口中定义的全部方法
客户端的请求通常是异步处理的,一般会返回 promise 的响应 ( 这主要归功于可以并行处理并响应多个请求 )
当然了处理异步请求/响应生命周期的需求超出了本规范的范围
现在尝试去定义客户端中间件还为时尚早
任何关注客户端请求处理的未来提案都应该有机会去定义一个特定于异步中间件的标准
有关更多信息,可以阅读文章底部相关链接中的 "client vs server side middleware"
中间件有什么用 ?
中间件有以下几个作用:
-
自行生成响应,如果满足特定的请求条件,中间件可以生成并返回响应
-
返回处理器的结果
在中间件无法自己生成响应的情况下,可以将请求交给请求处理器,由请求处理器来生成一个
当然了,中间件可以修改请求,例如注入请求属性或解析请求的内容
-
操作并返回请求处理器生成的响应
在满足特定的条件下,中间件可以修改处理器返回的响应,例如 gzip 压缩响应的内容,添加 CORS 头等等
在这种情况下,中间件会拦截请求处理器返回的响应,修改,并返回修改后的响应
后两种情况下,中间件的的代码实现可能如下
<?php // 直接返回处理器的结果 return $handler->handle($request); // 拦截处理器的结果 $response = $handler->handle($request);
只要处理器能够生成响应,处理器的代码实现完全由开发者自己决定
-
一种常见的做法是:
-
处理器在内部实现一个
_queue_
或_stack_
用于保存中间件实例 -
调用
$handler->handle($request)
会使得内部指针向前移动,并返回与该指针先关联的中间件 -
调用中间件的
process
方法处理请求$middleware->process($request, $this)
-
如果指针已经到了末尾,可以抛出一个异常或者返回备选的响应
-
-
另一种做法是:
- 使用 路由中间件 将传入的请求匹配到特定的请求处理器
- 然后返回该处理器生成的响应
- 如果没有匹配到请求处理器,则执行中间件所持有的处理器
- 这种机制甚至可以和第一种方法结合使用
6.3 交互接口范例
接口 RequestHandlerInterface
和 MiddlewareInterface
可以相互协同工作
在解耦任何任何重叠的应用程序时,中间件会更加灵活,因为它只依赖请求处理器来产生响应
接下来我们将讲述中间件调度系统的两种方法和一些可重用中间件的范例,以及演示如何开发解耦合的中间件
需要注意的是,这些不是推荐的最终或者排它性的设计中间件调度系统的方法
基于队列的请求处理器
在这种模式中,请求处理器维护一个中间件队列,然后按队列顺序执行每一个中间件,如果队列耗尽而没有生成响应,则返回一个备选响应
在执行第一个中间件时,队列会将自身作为请求处理程序传递给中间件
<?php 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); } }
下面是应用程序启动的范例代码
<?php // 备选处理器 $fallbackHandler = new NotFoundHandler(); // 创建一个队列处理器实例 $app = new QueueRequestHandler($fallbackHandler); // 添加一个或多个中间件 $app->add(new AuthorizationMiddleware()); $app->add(new RoutingMiddleware()); // 执行 $response = $app->handle(ServerRequestFactory::fromGlobals());
上面的范例有两个请求处理器:一个在最后一个中间件委托给请求处理程序时产生响应,另一个用于调度中间件
在这个范例中 RoutingMiddleware
可能会在匹配成功的执行组合的处理程序,更多内容请继续阅读下文
这种模式有以下几点好处:
-
中间件不需要关心其它中间件的存在,也不需要知道其它中间件如何运行
-
QueueRequestHandler
不用遵循 PSR 7 规范 -
中间件会按照它们被添加的顺序执行,这使得代码更加清晰
-
如何生成 「备选」响应完全取决于应用程序的开发者,这允许开发者决定是生成 "404 Not Found" 还是一个默认的页面
基于装饰器模式的请求处理器
在这种模式中,任何一个请求处理器都会持有中间件实例和下一个请求处理器实例
应用程序获取请求后,将请求传递给第一个请求处理器,请求处理器处理完之后会调用持有的下一个请求处理器
<?php 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());
与基于队列的中间件类似,请求处理程序在这种系统中有两个作用:
- 如果其它层没有生成响应,则生成一个备选响应
- 调度中间件
可重用的中间件范例
上面的范例中,我们创建了两个独立的中间件,但为了它们在任何一种情况下都能工作,我们就需要重写它们,以便它们能够进行交互
创建一个最大限度的可重用的中间件需要考虑以下指导原则:
-
对请求进行必要的条件测试,如果它不满足该条件,则生成一个最基本的响应或使用响应工厂方法来生成并返回响应
-
如果满足前置条件,可以委托请求处理程序来生成响应, 甚至可以通过修改提供的请求来选择性的提供一个 「新」 的请求
例如
$handler->handle($request->withAttribute('foo','bar'));
-
返回响应时,要么原样返回请求处理器生成的响应,要么对请求处理器返回的响应做一些更改,返回一个新的响应
相比于请求处理器返回的,就是新的了
例如
$response->withHeader('X-Foo-Bar', 'baz');
中间件 AuthorizationMiddleware
就很好的遵循了这三条指导原则
-
如果需要授权,但该请求未经授权,则将生成一个最基本的 「未授权」响应
-
如果不需要授权,它会将请求直接传递给请求处理器而不做任何更改
-
如果需要授权且请求被授权,它会将请求传递给请求处理器, 然后对返回的响应进行签名并返回签名后的响应
<?php class AuthorizationMiddleware implements MiddlewareInterface { private $authorizationMap; public function __construct(AuthorizationMap $authorizationMap) { $this->authorizationMap = $authorizationMap; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (! $authorizationMap->needsAuthorization($request)) { return $handler->handle($request); } if (! $authorizationMap->isAuthorized($request)) { return $authorizationMap->prepareUnauthorizedResponse(); } $response = $handler->handle($request); return $authorizationMap->signResponse($response, $request); } }
需要注意的是,中间件并不关心请求处理器如何实现,它仅仅是在满足前提条件的时候使用它来生成响应
下面代码中的 RoutingMiddleware
实现遵循类似的过程: 分析请求并查看是否匹配已经注册了的路由
在这个特定的实现中,路由被映射到请求处理程序,中间件实质上委托给它们产生响应
但是,如果没有匹配任何一个路由,中间件会将执行传递给它的处理程序来生成响应
<?php 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); } }
7. 参与人员
此 PSR 由 FIG 工作组的下列成员创建
- Matthew Weier O'Phinney (sponsor), mweierophinney@gmail.com
- Woody Gilk (editor), woody.gilk@gmail.com
- Glenn Eggleton
- Matthieu Napoli
- Oscar Otero
- Korvin Szanto
- Stefano Torresi
工作组还要感谢下列人员的贡献
- Jason Coward, jason@opengeek.com
- Paul M. Jones, pmjones88@gmail.com
- Rasmus Schultz, rasmus@mindplay.dk
8. 表决
9. 相关链接
根据时间顺序排列
- PHP-FIG mailing list thread
- The PHP League middleware proposal
- PHP-FIG discussion of FrameInterface
- PHP-FIG discussion about client vs server side middleware
10. 勘误表
...