PSR-15 HTTP请求消息处理程序的接口建议书解读
为什么会有PSR-15的出现
对于HTTP Request对象的处理,是任何一个 Web/API 应用程序都必须要面对的问题,但是却没有一个确定的参考规范,来约束如何实现对HTTP Request
进行操作的消息处理器和中间件类。
- 中间件:在PHP生态中已经存在并使用了多年,StackPHP 提出了一个可复用的中间件模块的通用模型。自从 PSR 提出了
HTTP Message
接口建议以来,众多的PHP框架已经基于HTTP Message
各自实现了中间件模块 - 遵从上面的标准而开发的中间件接口库,为
HTTP Request
带来了如下的好处:- 为开发人员提供正式标准 (使用这些库、方法的时候非常的方便)
- 允许任何中间件组件在任何兼容框架中运行 (中间件库可以独立开发,并在其他框架中被使用)
- 消除了在不同框架中,被重复定义的类似接口
- 避免方法的声明中的微小差异 (比如参数名称、类型、返回值类型等)
PSR-15 建议书包含的内容
涉及的问题
- 创建 "HTTP消息的请求处理程序" 的接口规范
- 创建 "HTTP消息的中间件" 的接口规范
- 基于最佳实践,按照 "请求处理程序 & 中间件的规范" 来实现这两个组件
- 确保 "请求处理程序 & 中间件" 与
HTTP Message
规范的任何实现都可以兼容
不涉及的问题
- 尝试定义如何创建HTTP响应的机制
- 尝试为客户端/异步中间件定义接口
- 试图定义如何转发中间件
"请求处理程序"组件的实现
不同的应用场景可能导致 Request Handler
有多重的用途,Request Handler
组件也被很多开发人员实现了很多次。但是无论思路有多么的不同,它们针对 HTTP Message
的处理过程都是一样的:
- 给定HTTP请求,为该请求生成HTTP响应。
"中间件"组件的实现
目前有两种使用HTTP消息的中间件的常用方法:
双向通道
这是大多数中间件的实现库使用的方法定义,基于 Express middleware
官方链接 的实现思路,定义如下:
fn(request, response, next): response
基于已采用此定义的中间件实现,可以观察到以下共性:
- 中间件被定义为可调用的。
- 中间件在调用期间传入3个参数:
- ServerRequestInterface 接口的对象
- ResponseInterface 接口的对象。
- 一个callable,它接收请求和响应委托给下一个中间件
业界已经有大量的实现库。这种方法通常被称为“双通”,指的是传递给中间件的参数中包括了 请求
和 响应
.
框架项目举例
- mindplay/middleman v1
- relay/relay v1
- slim/slim v3
- zendframework/zend-stratigility v1
中间件库举例
- 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
这个接口的主要缺点是,当中间件接口对象,本身是可调用的时,目前没有办法确定闭包参数
callable
的类型
单向通道 (Lambda)
这是另外一种中间件的实现库的方法定义,基于 StackPHP
的风格,定义如下:
fn(request, next): response
基于已采用此定义的中间件实现,可以观察到以下共性:
- 中间件接口定义中所包含的
HTTP Request
参数,用来做进一步的处理 - 中间件在调用期间传入2个参数:
- HTTP Request message 接口的对象
- 中间件可以委派另一个
Request Handler
生成HTTP响应消息
在这种形式中,中间件自身,在请求处理程序生成响应对象之前无法访问响应的。 在得到响应对象之后,中间件可以在修改响应之后再返回。
使用单通方式实现中间件的项目
- 使用这种方式的项目不多,但是却包含大名鼎鼎的 Guzzle, 我想不会是由于Guzzle过于优秀,大家觉得够了,不需要再浪费精力了吧。
// Guzzle的middleware的接口定义
function (RequestInterface $request, array $options): ResponseInterface
基于 Symfony HttpKernel 的StackPHP也是单向通道的,定义如下:
// StackPHP的middleware的接口定义, 注意是没有 response 参数的 function handle(Request $request, $type, $catch): Response
基于 Symfony 组件的 Laravel middleware也是其中之一, 定义如下
// Laravel的middleware的接口定义 function handle(Request $request, callable $next): Response
以上两种方式的现状
- 多年来,PHP社区已经大量的使用了"单通道中间件"方法, 特别是基于StackPHP的大量软件包,可以证明这一点
- 双通道方法更新,但采用HTTP消息(PSR-7)规范开发的库, 普遍使用这种方式
结论
尽管双通道方法几乎被普遍采用,但在实施方面存在重大问题。
最严重的是传递空响应并不能保证响应处于可用状态。中间件可以在将响应传递给进一步处理之前修改响应,这进一步加剧了这一点。
进一步使问题复杂化的是,无法确保未响应对象主体的完整性,这可能导致输出不完整或错误响应与附加的缓存头一起发送。如果新内容比原始内容短,则在写入现有正文内容时也可能最终导致损坏的正文内容。解决这些问题的最有效方法是在修改消息正文时始终提供新流。
有人认为通过响应有助于确保依赖倒置。虽然它确实有助于避免依赖于HTTP消息的特定实现,但也可以通过将工厂注入中间件来创建HTTP消息对象,或者通过注入空消息实例来解决问题。通过在PSR-17中创建HTTP工厂,可以实现处理依赖性反转的标准方法。
一个更主观但也很重要的问题是现有的双通中间件通常使用可调用类型提示来引用中间件。这使得严格的类型不可能,因为无法保证传递的可调用实现中间件签名,这降低了运行时的安全性。
为此, PSR-17 提案选择了lambda 单通方式最为中间件的最终实现方法。
解析: 建议书审议过程中的一些重要问题的讨论
RequestHandlerInterface
接口定义中只包含了一个方法handle()
, 并确保会返回一个ResponseInterface
接口对象实例- 请求处理器 (
RequestHandlerInterface
的对象实例) 可以代理给另外一个请求处理器对象
::: tip 这是一个很巧妙的设计思路, 在实际开发中, 需要配合public function handle(ServerRequestInterface $request): ResponseInterface;
装饰器设计模式
一起使用, 从而达到更强的功能, 举个例子: :::
首先有一个请求处理器实现类, 代码如下:
<?php
class DecoratingRequestHandler implements RequestHandlerInterface
{
private $middleware;
private $nextHandler;
/**
* 请求处理器的构造方法, 它需要传入两个参数
*
* @param MiddlewareInterface $middleware : 所依赖的中间件
* @param RequestHandlerInterface $nextHandler: 下一步需要的请求处理器对象
*/
public function __construct(
MiddlewareInterface $middleware,
RequestHandlerInterface $nextHandler
)
{
$this->middleware = $middleware;
$this->nextHandler = $nextHandler;
}
/**
* 实现接口要求的handle方法: 在实现过程中, 同时实现了"装饰器模式"的操作
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process(
$request,
$this->nextHandler // 装饰器模式
);
}
}
第二个请求处理器的实现类, 代码如下: ```php <?php class InnerRequestHandler implements RequestHandlerInterface { /**
* @var ResponseInterface $responsePrototype */ private $responsePrototype;
/**
* 请求处理器的构造方法, 它需要传入1个参数
*
* @param ResponseInterface $responsePrototype : HTTP 响应对象
*/
public function __construct(ResponseInterface $responsePrototype)
{
$this->responsePrototype = $responsePrototype;
}
/**
* 实现接口要求的handle方法
*
* @param ServerRequestInterface $request
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->responsePrototype;
}
}
> 有了以上的两个测试类, 我们可以编写出如下的代码, 并且利用的是装饰器模式的思路:
```php{35}
<?php
// 首先假定这个时候, 你的程序已经已经产生, 但是还没结束处理的 response 对象
$responsePrototype = (new Response())->withStatus(404);
// 我们构造一个 请求处理器对象的容器, 就是一个简单的数组
$handlers = [];
// 构造最内层的 请求处理器对象
$innerHandler = new InnerRequestHandler($responsePrototype);
array_push($handlers, $innerHandler);
// 第一层的处理: 用装饰器请求处理类 构造处理路由的处理器
$layer1 = new DecoratingRequestHandler(new RoutingMiddleware(), $innerHandler);
array_push($handlers, $layer1);
// 第二层的处理: 用装饰器请求处理类 构造认证的处理器
$layer2 = new DecoratingRequestHandler(new AuthenticationMiddleware(), $layer1);
array_push($handlers, $layer2);
// 第三层的处理: 用装饰器请求处理类 构造鉴权的处理器
$layer3 = new DecoratingRequestHandler(new AuthorizationMiddleware(), $layer2);
array_push($handlers, $layer3);
// ... 可以继续的添加更多的处理器, 当然要利用装饰器来进行构造
/**
* @var @RequestHandlerInterface $hdl
*/
$hdl = $handlers[count($handlers) - 1] ?? null;
if($hdl){
// 最后, 简单的使用最外层的处理器处理即可, 即可得到最终的响应对象
$response = $hdl->handle(ServerRequestFactory::fromGlobals());
}
// ...
通过上面的简单例子,可以看出, 基于PSR-15, 可以按照以下思路来实现服务器端的程序
- 维护一个
处理器对象的容器
- 利用装饰器模式的特点, 逐个将所需要的, 实现了特定功能的
请求处理器
放到容器中- 调用最外层的
请求处理器
的handle()
方法即可获得最终的响应数据
::: tip 这种思路可能有悖于你已经熟悉的MVC或者Laravel之类的框架. 尝试理解这个思路, 你会发现如果不依赖于那些框架, 你也可以构造优雅的程序, 甚至自己写一个 :::