PSR-15 的由来(写在 PSR-15 诞生之时)
昨天(写于 2018-01-23 ),经过核心委员会的一致表决,PHP-FIG 正式通过了 PSR-15, HTTP Server Handlers 标准的提案。
这个新标准为 请求处理器( request handlers ) 和 中间件( middleware ) 定义了接口。它们对 PHP 的生态系统有着巨大的潜在影响,因为它们为编写面向 HTTP 的服务端应用程序提供了标准的机制。实际上,它们为开发者在任何使用了 PSR-15 中间件或者请求处理器的程序中创建可复用的 web 组件铺了路。
备注
本人作为 PSR-15 的赞助商,同时也担任了审查期间的最终仲裁者。
背景
PSR-15 由 Woody Gilk 发起,在此期间他担任总编辑。最初的意图是制定一个中间件标准,并认为应该采用一个已经被广泛使用的模式:
function (
ServerRequestInterface $request,
ResponseInterface $response,
callable $next
) : ResponseInterface
参数 $next
应按如下方式实现:
function (
ServerRequestInterface $request,
ResponseInterface $response
) : ResponseInterface
"双通道"
上述模式被称作“双通道”中间件,因为它将两个实列传递给协作者并传递到下一层。
然而,对这一做法批评的声音很快就出现了, 其中有来自 Anthony Ferrara 的 ,主要的问题如下:
-
层对层之间传递响应可能会带来一些问题,当一个外部层对传递到内部层的响应进行更改,你期望外部层的更改能通过内部层后传播出去,但是内部层却返回了一个完全不同的响应。本质上,这种实现模式是有问题的,如果中间件需要操作响应,那么它应该基于另一层返回的响应进行操作。
-
参数
$next
类型为回调意味着没有办法确保它能接收到参数,换句话说,它不是类型安全的。
在工作组内部讨论之后,下一个迭代版本提出了以下方案(一些细节可能不同,但基本交互是相同的):
interface DelegateInterface
{
public function process(ServerRequestInterface $request) : ResponseInterface;
}
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
DelegateInterface $delegate
) : ResponseInterface;
}
这在很大程度上解决了上面提到的问题。然而,不同的团队会出现一些不同的实现细节。
首先,许多人指出定义相同的方法名可以防止多态性。通常的做法是定义一个可以调用的「请求处理程序」并会反过来加工它本身。所以,我们更新了接口如下:
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request) : ResponseInterface;
}
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
) : ResponseInterface;
}
其次,这个变更确定后,一部分人意识到请求处理器本身是有用的。例如,当创建一个简单的站点时,你可以处理一个服务请求,将其传递给一个处理器,然后发出返回的响应;在这种情况下中间件可能不是必须的。另一个使用场景是针对最终的内部的中间件应用程序:你可以将它们实现为请求处理器,而不是作为中间件,因为它们不操作处理器的结果。
因此,我们将这两个接口调整为 独立的包,包含 MiddlewareInterface
的包依赖于定义了 RequestHandlerInterface
的包。
最后,在本规范开发的近两年时间里,随着 7.1 版本和 7.2 版本的发布, PHP 7 变得成熟了。我们决定将规范推向 PHP7 或者更高版本,并且正式采用返回类型提示。
尽管本规范的工作尚在进程当中,但接口的每一次迭代都发布在 http-interop 这个 github 组织里了,每次发布,这些包都与任何当前规范详细匹配(先是 http 中间件,然后是 http-server 中间件,甚至是添加 http-server 处理器)。这些包也使用 Interop\Http
作为顶级命名空间。工作组成员及其他感兴趣的团体,将把他们的贡献推向特定的迭代中去。
最终包归 PHP-FIG 团队所有,并且使用 Psr
作为顶级命名空间。
接口
于是我们有了最终的标准:
- PSR-15
- PSR-15 Meta Document (在规范后面包含了 为什么 的说明)
psr/http-server-handler 提供了以下接口:
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request) : ResponseInterface;
}
psr/http-server-middleware 提供了以下接口:
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
) : ResponseInterface;
}
两个包都依赖 PSR-7,因为需要用到该包中定义的 HTTP 消息接口作为类型提示。http 服务中间件(http-server-middleware) 包依赖于 http 服务处理器(http-server-handler) 包。
如何写出可复用的中间件
目前可用的绝大多数中间件转发器(事实证明,确实有很多!)都允许你以一种方式来组合中间件,在这种方式下,转发器无需了解它们是由什么东西如何组装起来的。这是件好事情™。 这样你写的中间件就能和使用它的上下文去耦合了。
但是要怎么做呢?
在规范的说明文档中,我们 有如下建议:
-
要测试请求所必须的前提条件(如果有的话)。如果不满足其中任何一项,就使用组合的 响应原型(response prototype) 或者 响应工厂(response factory) 来生成并返回响应。
-
如果前提条件满足了,就对提供的处理程序委托创建的响应(PSR-7 的请求是固定的,因此这意味着调用一个
with*()
方法就行了,该方法会返回一个新的实例)。 -
要么从处理程序逐字传回响应,要么操纵它的返回值返回一个新的响应( 再次通过
with*()
方法)。
第一点可能是最重要的一点:不要在你的中间件里直接实例化一个响应,而是使用实例化期间提供的一个 原型(prototype) 或者 一个 工厂(factory) 来代替。这能让你的中间件与程序中使用的 PSR-7 的实现去耦合。
实践中代码如下:
class CheckOriginMiddleware implements MiddlewareInterface
{
private $acceptedOrigins;
private $responsePrototype;
public function __construct(array $acceptedOrigins, ResponseInterface $responsePrototype)
{
$this->acceptedOrigins = $acceptedOrigins;
$this->responsePrototype = $responsePrototype;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$origin = $request->getHeaderLine('origin');
if (! in_array($origin, $this->acceptedOrigins, true)) {
return $this->responsePrototype
->withStatus(401)
->withHeader('X-Invalid-Origin', $origin);
}
$response = $handler->handle($request);
return $response->withHeader('X-Origin', $origin);
}
}
关于这个中间件,有几点需要注意:
-
它只通过构造器来接收依赖的类,这有助于提高此中间件的可测试性,并且所需依赖一目了然。
-
返回的数据原型已经与 PSR-7 解耦。我可以传输一个 Diactoros 返回,Guzzle 返回,Slim 返回, 或者其他的实现。如此一来,此中间件的使用者就不用安装另一个的 PSR-7 实现了。
-
此中间件对其使用的场景,以及其调用堆栈毫无所知。它只知道处理请求,当
process()
被调用时即返回处理器。
我该如何消费这样的中间件呢?
在 Expressive 中,我可能会做以下任何一项:
// 把它当做一种服务来从 DI 容器中拉出来:
$app->pipe(CheckOriginMiddleware::class);
// 在特定的路由管道中使用它:
$app->post('/api/foo', [
CheckOriginMiddleware::class,
FooMiddleware::class,
]);
在 northwoods/broker 中,(由 Woody Gilk 维护,他是 PSR-15 的编辑),看起来像这样:
$broker->always([CheckOriginMiddleware::class]);
在 middlewares/utils Dispatcher 中,你需要这样做:
$dispatcher = new Dispatcher([
/* ... */
new CheckOriginMiddleware($acceptedOrigins, $responsePrototype),
/* ... */
]);
采用上述任何解决办法,只要你的中间件被执行了,那么它的表现都是一样的;它是 如何 组合的并不影响,因为它的运作方式只依赖于在 process()
期间传递给中间件的请求和处理器。
请求处理器又怎么样呢?
目前我见过的大多数库都是使用以下两种方式之一来定义处理器的:
-
作为一个中间件转发器。在这种特定情况下,每个中间件都会被处理,直到其中一个返回了响应为止。如果最后一个被处理的中间件又调用了处理器,那么可能会返回一个预定的响应,或者抛出一个异常,又或者下一个场景会发挥作用:
-
作为一个传递给中间件转发器的『终极』处理器。 换句话说,假设最后一个被处理的中间件 也 调用了它的处理器,这个『回退』 或者说 『终极』处理器就是那个被调用的。这个根据实现不同,一般会返回一个 404 响应或者一个 500 响应。
另一种可能是由多个 路由中间件 所使用。在这种情况下,一旦路由中间件匹配到了请求,它就会调用该请求映射到的那个请求处理器。
实现须知
PSR-15 已经定案;来,我们把所有的东西 PSR-15 化吧!
稍安勿躁!因为许多项目中仍旧在使用 http-interop 包进行持续迭代,这使得实现 PSR-15 规范化可能是一个长期目标。
举个例子,我们已经在 Stratigility 和 Expressive 跟踪了 http-interop 各种版本的迭代,但是升级到 PSR-15 规范需要做向后兼容, 所以需要出一个新的 3.0 版本——这要花上好几周的时间。 Slim 的 另一个 PSR-15 支持补丁,在即将到来的 4.0 版本发布之前,这个事也不会落地。
因此,我们应该给库和框架维护者一些时间和耐心,并帮助他们测试发布版。
另外,考虑下跟踪和测试 PSR-17 规范提案。 该提案将规范化 PSR-7 的 工厂,这些工厂将为生成中间件,尤其是返回响应,提供一种标准的方式。使用组合工厂来代替组合响应原型。那么为啥这样子会更轻松呢? 好吧,万一你想在某个地方放置 Psr\Http\Message\StreamInterface
的实例作为 响应体, 这种方式也是允许你创建那些实例的。由于流不是固定的(因为语言的限制),所以每次你向流写入东西的时候,你可以追加现有的内容,也就是说,中间件写入到响应原型体的时候通常也需要组合一个 流 原型。 这时候,如果你能组合一个单独的工厂来代替的话会怎样呢?
结论
最初当我开始为 PSR-7 标准工作的时候,是因为我希望能有一个标准的 PHP 中间件接口。我曾经使用过 Node ,更具体的说,是 Sencha Connect 和 ExpressJS。Node 一直以来都有着强大的中间件生态系统。形成这种情况的原因有两点:
-
广泛接受的,标准的中间件签名。 虽然 JS 官方并没有提供相关接口,而且也没有用户层的标准组织,就这样出现了一个统一的签名,然后每个人都使用它。我想这大概是因为:
-
Node 核心库中内置的 HTTP 消息抽象 。
如果我想在 PHP 中实现标准的中间件,首先我们需要标准的 HTTP 消息, PSR-7 已经实现了这一点。也许可以到此为止,因为许多类库已经开始使用相同的中间件签名;然而,我们很快有了至少两种,也有可能是六种不同的实现。令人感谢的是,Woody 挺身而出面对这个任务并且提出了 PSR-15 标准的草案;而且,他和工作小组的其他成员一起,靠着他们的耐心和毅力最终目睹了标准的通过 (虽然据我所知有好几次他和及另外几个人差点就放弃了!)。
随着 PSR-15 标准的通过, 我们又朝着我曾经的设想近了一步:可能有一天,PHP 开发者们不再需要使用重量级的 MVC 框架, 而是用丰富的,可复用的中间件来构建应用程序。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: