昨天,经过其核心委员会的一致投票,PHP-FIG正式接受了提议的PSR-15,即HTTP服务器处理程序标准。
这个新标准定义了请求处理程序和中间件的接口。它们对PHP生态系统具有巨大的潜在影响,因为它们提供了编写面向HTTP、服务器端的标准机制。端应用程序。从本质上讲,它们为开发人员创建可重复使用的Web组件铺平了道路,这些组件将在任何使用PSR-15中间件或请求处理程序的应用程序中工作!
警告
我是PSR-15的发起人,也是审查期间变更的最终仲裁者。
警告
我是PSR-15的发起人,也是审查期间变更的最终仲裁者。
背景
PSR-15由WoodyGilk发起,他在整个期间担任编辑。最初的目的是批准中间件标准,最初认为这将是对已经广泛使用的模式的快速批准:
function ( ServerRequestInterface $request, ResponseInterface $response, callable $next ) : ResponseInterface
$next
应该实现以下签名:
function ( ServerRequestInterface $request, ResponseInterface $response ) : ResponseInterface
“双重传递”
上述模式被称为“双重传递”中间件,因为它将两个实例传递给合作者以传递到下一层。
“双重传递”
上述模式被称为“双重传递”中间件,因为它将两个实例传递给合作者以传递到下一层。
然而,对这种现有做法的一些批评几乎立即开始出现,其中来自AnthonyFerrara的批评尤为重要。注意到的主要问题是:
-
将响应从一层传递到另一层可能会导致外层更改它传递给内层的响应,期望它传播回来,但内层返回不同的响应完全。从本质上讲,该模式促进了有问题的做法。如果中间件需要对响应进行操作,它应该对另一层返回的响应进行操作。
-
类型提示
$next
ascallable意味着没有办法确保thecallable实际上能够接受传递给它的参数。换句话说,它不是类型安全的。
经过工作组内部的讨论,下一次迭代提出了以下内容(一些细节不同,但基本交互是相同的):
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.
最后,在制定该规范的近两年时间里,PHP7逐渐成熟,发布了7.1和7.2版本。我们决定将该规范固定到PHP7或更高版本,并在其中正式采用返回类型提示。
在制定规范的过程中,接口的每次迭代都在github组织http-interop中发布,包匹配当前规范的任何详细信息(http-middleware,然后是http-server-middleware,最后添加http-服务器处理程序)。这些包还使用Interop\Http
作为顶级命名空间。工作组的成员以及其他感兴趣的各方会将他们的产品固定到特定的迭代。
不过,最终的包现在归PHP-FIG组所有,并使用Psr
顶级命名空间。
接口
这给我们带来了最终标准:
- PSR-15
- PSR-15元文档(涵盖规范背后的原因)
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-server-middleware包依赖于http-server-handler包。
如何编写可重用的中间件
目前可用的大多数中间件调度程序(事实证明,它们有很多!)允许您以这样一种方式组合中间件,而无需知道它是如何或由什么构成的。这是一件好事™。它允许您编写与使用它的上下文分离的中间件。
但是你是怎么做到的呢?
在规范的元文档中,我们建议如下:
-
测试所需前提条件的请求(如果有)。如果它不满足任何条件,则使用组合的响应原型或响应工厂生成并返回响应。
-
如果预-满足条件,委托创建对提供的处理程序的响应,可选地提供“新”请求(PSR-7请求是不可变的,因此这意味着调用其中一个
with*()
方法,这returnnewinstances). -
要么从处理程序逐字传回响应,要么通过操作返回的响应返回一个新的响应(同样,通过
with*()
方法)。
这里的第一点可能是最重要的:不要直接在中间件中实例化响应,而是使用实例化期间提供的原型或工厂。这允许您将中间件与应用程序使用的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响应、Slimresponse或任何其他实现。因此,这个中间件的消费者将不需要潜在地安装另一个PSR-7实现。
-
中间件不知道它将在哪里使用,或者它将是什么堆栈用在。它只是对调用
process()
时提供的请求和处理程序进行操作。
我如何使用这样的中间件?
在Expressive中),我可能会执行以下任一操作:
// Pipe it as a service to pull from the DI container: $app->pipe(CheckOriginMiddleware::class); // Use it within a route-specific pipeline: $app->post('/api/foo', [ CheckOriginMiddleware::class, FooMiddleware::class, ]);
在northwoods/broker(由PSR-15编辑WoodyGilk维护)中,它看起来像这样:
$broker->always([CheckOriginMiddleware::class]);
在middlewares/utilsDispatcher中,你会这样做:
$dispatcher = new Dispatcher([ /* ... */ new CheckOriginMiddleware($acceptedOrigins, $responsePrototype), /* ... */ ]);
使用这些解决方案中的任何一种,如果您的中间件被执行,它的行为将完全相同;如何它的组成并不重要,因为它的操作方式仅取决于在process()
期间传递给中间件的请求和处理程序。
请求处理程序呢?
目前我看过的大多数库都以两种方式之一定义处理程序:
-
作为中间件调度程序。在这种特殊情况下,每个中间件都会被处理,直到其中一个返回响应。如果最后一个处理程序再次调用处理程序,则返回固定响应,抛出异常,或者下一个场景开始发挥作用:
-
作为“最终”处理程序传递给中间件调度程序。换句话说,如果最后一个处理的中间件也调用它的处理程序,这个“后备”或“最终”处理程序将被调用。这通常会返回404响应或500响应,具体取决于实施情况。
其他一些人提出的可能性是与routingmiddleware一起使用。在这种情况下,当路由中间件匹配请求时,它会调用映射到该请求的请求处理程序。
实施者须知
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工厂,这将为中间件生成(特别是返回响应)提供标准方式。您可以构建一个工厂,而不是构建一个响应原型。为什么这更容易?好吧,在您可能还想处理响应body的情况下,它是一个Psr\Http\Message\StreamInterface
实例,它允许您创建这些的新实例也是如此。由于流不可能是不可变的(由于语言限制),任何时候写入流时,您都可以附加现有内容,这意味着写入响应原型正文的中间件通常还需要组成一个流原型。如果您可以改为组合单个工厂会怎么样?
结束语
当我最初开始研究PSR-7时,那是因为我想要一个用于PHP的标准中间件接口。我一直在玩Node,更具体地说,是SenchaConnect和ExpressJS。Node中的中间件生态系统曾经并将继续是巨大的。它存在的原因有两个:
-
已接受,标准中间件签名。尽管JS不提供接口,也没有userland标准体,但一个共识签名出现了,每个人都使用它。这些之所以成为可能,是因为:
-
Node核心库中的内置HTTP消息抽象。
如果我想要PHP中的标准中间件,我们首先需要标准的HTTP消息,PSR-7已完成。这本来可以结束,因为许多库开始使用相同的中间件签名;然而,我们很快就有了至少两种,可能多达六种不同的方法。值得庆幸的是,Woody挺身而出,提出了成为PSR-15的方案;此外,他和工作组的其他成员有耐心和毅力让它被接受(尽管我知道有好几次他和其他人差点认输!)。
随着PSR-15的接受,我们离我长期以来的设想又近了一步:PHP开发人员不再在单一的MVC框架中工作的可能性,而是从商品、可重用的中间件中组合应用程序。