PSR-15元文档

HTTP服务器请求处理程序元文档

1.总结

此PSR的目的是为HTTP服务器请求处理程序(“请求处理程序”)和HTTP服务器请求中间件(“中间件”)定义正式接口,这些接口与PSR-7或后续替换PSR中定义的HTTP消息兼容

注意:对“请求处理程序”和“中间件”的所有引用都特定于 服务器请求处理。

为什么要打扰?

HTTP消息规范不包含对请求处理程序或中间件的任何引用。

请求处理程序是任何Web应用程序的基础部分。处理程序是接收请求并生成响应的组件。几乎所有使用HTTP消息的代码都会有某种请求处理程序。

中间件已经在PHP生态系统中存在多年。StackPHP推广了可重用中间件的一般概念自从作为PSR发布HTTP消息以来,许多框架都采用了使用HTTP消息接口的中间件。

同意正式请求处理程序和中间件接口消除了几个问题,并具有许多好处:

  • 为开发人员提供正式标准。
  • 允许任何中间件组件在任何兼容框架中运行。
  • 消除了由各种框架定义的类似接口的重复。
  • 避免方法签名中的微小差异。

3.范围

3.1目标

  • 创建使用HTTP消息的请求处理程序接口。
  • 创建使用HTTP消息的中间件接口。
  • 实现基于最佳实践的请求处理程序和中间件签名。
  • 确保请求处理程序和中间件与HTTP消息的任何实现兼容。

3.2非目标

  • 尝试定义创建HTTP响应的机制。
  • 尝试为客户端/异步中间件定义接口。
  • 试图定义如何分派中间件。

4.请求处理程序方法

有许多方法可以请求使用HTTP消息的处理程序。但是,一般过程在所有过程中都是相同的:

给定HTTP请求,为该请求生成HTTP响应。

该过程的内部要求因框架和应用程序而异。该提案不会确定该流程应该是什么。

5.中间件方法

目前有两种使用HTTP消息的中间件的常用方法。

5.1双人通行证

大多数中间件实现使用的签名大致相同,基于Express中间件,定义如下:

fn(request, response, next): response

基于已采用此签名的框架已经使用的中间件实现,观察到以下共性:

  • 中间件被定义为可调用的
  • 中间件在调用期间传递3个参数:
    1. 一个ServerRequestInterface实现。
    2. 一个ResponseInterface实现。
    3. callable接收请求和响应委托给下一个中间件的A.

大量项目提供和/或使用完全相同的界面。这种方法通常被称为“双通”,指的是传递给中间件的请求和响应。

5.1.1使用双通道的项目

5.1.2中间件实现双通道

这个接口的主要缺点是,当接口本身是可调用的时,目前没有办法严格键入一个闭包。

5.2单通(Lambda)

中间件的另一种方法更接近StackPHP风格,定义为:

fn(request, next): response

采用这种方法的中间件通常具有以下共性:

  • 中间件使用特定接口定义,该接口具有接受处理请求的方法。
  • 中间件在调用期间传递了2个参数:
    1. HTTP请求消息。
    2. 中间件可以委派生成HTTP响应消息的责任的请求处理程序。

在这种形式中,中间件在请求处理程序生成响应之前无法访问响应。然后,中间件可以在返回之前修改响应。

这种方法通常被称为“单通”或“lambda”,仅涉及传递给中间件的请求。

5.2.1使用单程的项目

在使用HTTP消息的项目中,此方法的示例较少,但有一个值得注意的例外。

Guzzle中间件专注于传出(客户端)请求并使用此签名:

function (RequestInterface $request, array $options): ResponseInterface

5.2.2使用单程的附加项目

还有一些重要的项目使用这种方法抢占HTTP消息。

StackPHP基于Symfony HttpKernel并支持具有此签名的中间件:

function handle(Request $request, $type, $catch): Response

注意:虽然Stack有多个参数,但不包括响应对象。

Laravel中间件使用Symfony组件并支持具有此签名的中间件:

function handle(Request $request, callable $next): Response

5.3方法比较

多年来,PHP社区已经很好地建立了单通道中间件方法。对于基于StackPHP的大量软件包,这一点最为明显。

双通道方法更新,但早期采用HTTP消息(PSR-7)几乎普遍使用。

5.4选择方法

尽管双通道方法几乎被普遍采用,但在实施方面存在重大问题。

最严重的是传递空响应并不能保证响应处于可用状态。中间件可以在将响应传递给进一步处理之前修改响应,这进一步加剧了这一点。

进一步使问题复杂化的是,没有办法确保没有写入响应主体,这可能导致输出不完整或错误响应与附加的缓存头一起发送。如果新内容比原始内容短,则在写入现有正文内容时也可能最终导致损坏的正文内容解决这些问题的最有效方法是在修改消息正文时始终提供新流。

有人认为通过响应有助于确保依赖倒置。虽然它确实有助于避免依赖于HTTP消息的特定实现,但也可以通过将工厂注入中间件来创建HTTP消息对象,或者通过注入空消息实例来解决问题。通过在PSR-17中创建HTTP工厂,可以实现处理依赖性反转的标准方法。

更主观但也很重要的是,现有的双通中间件通常使用callable类型提示来指代中间件。这使得严格的输入成为不可能,因为无法保证callable 传递实现中间件签名,这会降低运行时的安全性。

由于这些重要问题,已为此提案选择了lambda方法。

6.设计决策

6.1请求处理程序设计

RequestHandlerInterface定义了接受请求并必须返回一个响应一个方法。请求处理程序可以委托给另一个处理程序。

为什么需要服务器请求?

要明确请求处理程序只能在服务器端上下文中使用。在客户端上下文中,可能会返回承诺而不是响应。

为什么用“处理程序”这个词?

术语“处理程序”表示指定用于管理或控制的内容。在请求处理方面,请求处理程序是必须对请求执行操作以创建响应的点。

与在本规范的先前版本中使用的术语“委托”相反,未指定此接口的内部行为。只要请求处理程序最终生成响应,它就是有效的。

为什么请求处理程序不使用__invoke?

使用__invoke不如使用命名方法透明。它还可以在将请求处理程序分配给类变量时更轻松地调用它,而无需使用call_user_func使用其他不太常见的语法。

有关其他信息,请参阅Frame -terface的PHP-FIG讨论

6.2中间件设计

MiddlewareInterface限定,它接受的HTTP请求和请求处理程序和必须返回一个响应的单一方法。中间件可能:

  • 在将请求传递给请求处理程序之前进化该请求。
  • 在返回之前,演变从请求处理程序接收的响应。
  • 创建并返回响应,而不将请求传递给请求处理程序,从而处理请求本身。

当按顺序从一个中间件委托给另一个中间件时,调度系统的一种方法是使用组成中间件序列的中间请求处理程序作为将中间件链接在一起的方式。最终或最里面的中间件将充当应用程序代码的网关,并从其结果中生成响应; 或者,中间件可以将此职责委托给专用的请求处理程序。

为什么中间件不使用__invoke?

这样做会与实现双通道方法的现有中间件冲突,并且可能希望实现中间件接口以便与此规范向前兼容。

为什么名称进程()?

我们回顾了许多现有的单片和中间件框架,以确定每个为处理传入请求而定义的方法。我们发现以下常用:

  • __invoke (在中间件系统中,如Slim,Expressive,Relay等)
  • handle(特别是从Symfony的HttpKernel派生的软件
  • dispatch(Zend Framework的DispatchableInterface

我们选择允许这些类的前向兼容方法将自己重新用作中间件(或与此规范兼容的中间件),因此需要选择不常用的名称。因此,我们选择 process表示处理请求。

为什么需要服务器请求?

清楚地表明中间件只能在同步的服务器端上下文中使用。

虽然并非所有中间件都需要使用服务器请求接口定义的其他方法,但出站请求通常是异步处理的,并且通常会返回响应承诺(这主要是因为多个请求可以并行处理并在返回时进行处理。)满足异步请求/响应生命周期需求超出了本提案的范围。

此时尝试定义客户端中间件还为时过早。任何关注客户端请求处理的未来提案都应该有机会定义一个特定于异步中间件性质的标准。

有关其他信息,请参阅有关客户端与服务器端中间件的PHP-FIG讨论

请求处理程序的作用是什么?

中间件具有以下角色:

  • 自己做出回应。如果满足特定请求条件,则中间件可以生成并返回响应。

  • 返回请求处理程序的结果。在中间件无法产生自己的响应的情况下,它可以委托给请求处理程序来生成一个; 有时这可能涉及提供转换的请求(例如,注入请求属性,或解析请求主体的结果)。

  • 操作并返回请求处理程序生成的响应。在一些情况下,中间件可能对操纵请求处理程序返回的响应感兴趣(例如,gzip响应主体,添加CORS头等)。在这种情况下,中间件将捕获请求处理程序返回的响应,并在完成时返回已转换的响应。

在后两种情况下,中间件可能具有如下代码:

// Straight delegation:
return $handler->handle($request);

// Capturing the response to manipulate:
$response = $handler->handle($request);

处理程序的行为完全取决于开发人员,只要它产生响应即可。

在一个常见场景中,处理程序在内部实现一个队列一堆中间件实例。在这种情况下,调用 $handler->handle($request)将推进内部指针,拉出与该指针关联的中间件,并使用它来调用它 $middleware->process($request, $this)如果不存在更多中间件,则通常会引发异常或返回预设响应。

另一种可能性是 将与传入服务器请求匹配的中间件路由到特定处理程序,然后返回通过执行该处理程序生成的响应。如果无法路由到处理程序,它将改为执行提供给中间件的处理程序。(这种机制甚至可以与中间件队列和堆栈一起使用。)

6.3接口交互示例

这两个接口,RequestHandlerInterface并且MiddlewareInterface,被设计为一起工作,彼此。中间件在从任何总体应用层解耦时获得灵活性,而只依赖于提供的请求处理程序来产生响应。

下面说明了工作组观察和/或实施的两种中间件调度系统方法。此外,还提供了可重用中间件的示例,以演示如何编写松散耦合的中间件。

请注意,这些不是建议用作定义中间件调度系统的权威或排他性方法。

基于队列的请求处理程序

在这种方法中,请求处理程序维护一个中间件队列,如果队列耗尽而没有返回响应,则返回一个回退响应。执行第一个中间件时,队列将自身作为请求处理程序传递给中间件。

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
    {
        // Last middleware in the queue has called on the request handler.
        if (0 === count($this->middleware)) {
            return $this->fallbackHandler->handle($request);
        }
        
        $middleware = array_shift($this->middleware);
        return $middleware->process($request, $this);
    }
}

应用程序引导程序可能如下所示:

// Fallback handler:
$fallbackHandler = new NotFoundHandler();

// Create request handler instance:
$app = new QueueRequestHandler($fallbackHandler);

// Add one or more middleware:
$app->add(new AuthorizationMiddleware());
$app->add(new RoutingMiddleware());

// execute it:
$response = $app->handle(ServerRequestFactory::fromGlobals());

该系统有两个请求处理程序:一个将在最后一个中间件委托给请求处理程序时生成响应,另一个用于调度中间件层。(在此示例中,RoutingMiddleware可能会在成功的路由匹配上执行组合处理程序;请参阅下面的详细信息。)

这种方法有以下好处:

  • 中间件不需要知道任何其他中间件或它在应用程序中的组成方式。
  • QueueRequestHandler是不可知使用PSR-7的实施。
  • 中间件按照添加到应用程序的顺序执行,使代码显式化。
  • 生成“后备”响应将委托给应用程序开发人员。这允许开发人员确定是否应该是“404 Not Found”条件,默认页面等。

基于装饰的请求处理程序

在这种方法中,请求处理程序实现装饰中间件实例和回退请求处理程序以传递给它。应用程序是从外向内构建的,将每个请求处理程序“层”传递给下一个外层。

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);
    }
}

// Create a response prototype to return if no middleware can produce a response
// on its own. This could be a 404, 500, or default page.
$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'))来提供“新”请求

  • 要么不加改变地传递请求处理程序返回的响应,要么通过操纵返回的响应来提供新的响应(例如,return $response->withHeader('X-Foo-Bar', 'baz'))。

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);
    }
}

请注意,中间件不关心请求处理程序的实现方式; 它只是在满足前提条件时使用它来产生响应。

下面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);
    }
}

7.人

该PSR由FIG工作组制作,成员如下:

工作组还要感谢以下方面的贡献:

8.投票

注意:顺序按时间顺序递减。

10.勘误表

...