翻译进度
15
分块数量
6
参与人数

PSR-15 HTTP 请求处理器 - 说明文档 本文未发布 发布文章

这是一篇协同翻译的文章,你可以点击『我来翻译』按钮来参与翻译。

请注意单词拼写,以及中英文排版,参考此页,特别注意以下:
  • 英文的左右 必须 保持一个空白,避免中英文字黏在一起;
  • 必须 使用全角标点符号;
  • 必须 严格遵循 Markdown 语法;
  • 原文中的双引号(" ")请代换成中文的引号(「」符号怎么打出来见 这里);
  • 加亮」和「加粗」和「[链接]()」都需要在左右保持一个空格;
  • 代码注释统一要求翻译;
请不要把翻译工具(如百度翻译)里翻译的内容直接提交,此类行为会被视为对其他译者的 不尊重; 翻译时请特别注意排版规范,如果你的翻译不遵循排版规范,我们会很痛心的拒绝你的提交。

HTTP 服务器请求处理器文档

1. 概要

这个 PSR 的目的是为 HTTP 服务器请求处理程序(「request handlers」「请求处理程序」)和HTTP服务器请求中间件( 「middleware」「中间件」)定义正式的接口。它们和 PSR-7 中定义的消息或后续替代的 PSRs 兼容。

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

2. 不足之处?

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

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

中间件 在 PHP 的生态系统中已经存在好多年了。可重用中间件的概念在 StackPHP 中比较普及。自从 HTTP 消息作为 PSR 发布以来,许多框架采用HTTP消息接口作为中间件。

正式的请求处理程序和中间件接口达成一致消除了许多问题并具备许多有点。

  • 为开发人员提供了正式标准。。
  • 使任何中间件组件在任何框架都能兼容。
  • 消除框架相似接口的重复定义。
  • 避免方法签名中的细微差别。
clifford 翻译于 5年前

3. 作用域

3.1 目标

  • 创建一个符合 HTTP 消息规范的请求处理器接口
  • 创建一个符合 HTTP 消息规范的中间件接口
  • 根据最佳实战原则实现处理器和中间件
  • 确保请求处理器与中间件和 HTTP 消息规范的任何实现相兼容

3.2 非目标

  • 尝试定义一种机制用于选择哪种 HTTP 响应该被创建
  • 尝试定义客户端/异步中间件
  • 尝试定义中间件如何被调度

4. 请求处理器方法

有多种方式可以创建一个符合 HTTP 消息 ( PSR 7 ) 规范的请求处理器。但它们都有一个相同的处理过程::

接受一个 HTTP 请求,并生成相对应的 HTTP 响应

该提案不会确定如何实现一个请求处理器的内部逻辑,处理器的内部实现可以框架和应用而各不相同

Jliu 翻译于 5年前

5. Middleware Approaches

There are currently two common approaches to middleware that use HTTP messages.

5.1 Double Pass

The signature used by most middleware implementations has been mostly the same and is based on Express middleware, which is defined as:

fn(request, response, next): response

Based on the middleware implementations already used by frameworks that have adopted this signature, the following commonalities are observed:

  • The middleware is defined as a callable.
  • The middleware is passed 3 arguments during invocation:
    1. ServerRequestInterface implementation.
    2. ResponseInterface implementation.
    3. callable that receives the request and response to delegate to the next middleware.

A significant number of projects provide and/or use exactly the same interface. This approach is often referred to as "double pass" in reference to both the request and response being passed to the middleware.

5.2 Single Pass (Lambda)

The other approach to middleware is much closer to StackPHP style and is defined as:

fn(request, next): response

Middleware taking this approach generally has the following commonalities:

  • The middleware is defined with a specific interface with a method that takes the request for processing.
  • The middleware is passed 2 arguments during invocation:
    1. An HTTP request message.
    2. A request handler to which the middleware can delegate the responsibility of producing an HTTP response message.

In this form, middleware has no access to a response until one is generated by the request handler. Middleware can then modify the response before returning it.

This approach is often referred to as "single pass" or "lambda" in reference to only the request being passed to the middleware.

5.2.1 Projects Using Single Pass

There are fewer examples of this approach within projects using HTTP messages, with one notable exception.

Guzzle middleware is focused on outgoing (client) requests and uses this signature:

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

5.2.2 Additional Projects Using Single Pass

There are also significant projects that predate HTTP messages using this approach.

StackPHP is based on Symfony HttpKernel and supports middleware with this signature:

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

Note: While Stack has multiple arguments, a response object is not included.

Laravel middleware uses Symfony components and supports middleware with this signature:

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

5.3 Comparison of Approaches

The single pass approach to middleware has been well established in the PHP community for many years. This is most evident with the large number of packages that are based around StackPHP.

The double pass approach is much newer but has been almost universally used by early adopters of HTTP messages (PSR-7).

5.4 Chosen Approach

Despite the nearly universal adoption of the double-pass approach, there are significant issues regarding implementation.

The most severe is that passing an empty response has no guarantees that the response is in a usable state. This is further exacerbated by the fact that a middleware may modify the response before passing it for further processing.

Further compounding the problem is that there is no way to ensure that the response body has not been written to, which can lead to incomplete output or error responses being sent with cache headers attached. It is also possible to end up with corrupted body content when writing over existing body content if the new content is shorter than the original. The most effective way to resolve these issues is to always provide a fresh stream when modifying the body of a message.

Some have argued that passing the response helps ensure dependency inversion. While it is true that it helps avoid depending on a specific implementation of HTTP messages, the problem can also be resolved by injecting factories into the middleware to create HTTP message objects, or by injecting empty message instances. With the creation of HTTP Factories in PSR-17, a standard approach to handling dependency inversion is possible.

A more subjective, but also important, concern is that existing double-pass middleware typically uses the callable type hint to refer to middleware. This makes strict typing impossible, as there is no assurance that the callable being passed implements a middleware signature, which reduces runtime safety.

Due to these significant issues, the lambda approach has been chosen for this proposal.

6. Design Decisions

6.1 Request Handler Design

The RequestHandlerInterface defines a single method that accepts a request and MUST return a response. The request handler MAY delegate to another handler.

Why is a server request required?

To make it clear that the request handler can only be used in a server side context. In an client side context, a promisewould likely be returned instead of a response.

Why the term "handler"?

The term "handler" means something designated to manage or control. In terms of request processing, a request handler is the point where the request must be acted upon to create a response.

As opposed to the term "delegate", which was used in a previous version of this specification, the internal behavior of this interface is not specified. As long as the request handler ultimately produces a response, it is valid.

Why doesn't request handler use __invoke?

Using __invoke is less transparent than using a named method. It also makes it easier to call the request handler when it is assigned to a class variable, without using call_user_func or other less common syntax.

See "discussion of FrameInterface" in relevant links for additional information.

6.2 Middleware Design

The MiddlewareInterface defines a single method that accepts an HTTP request and a request handler and must return a response. The middleware may:

  • Evolve the request before passing it to the request handler.
  • Evolve the response received from the request handler before returning it.
  • Create and return a response without passing the request to the request handler, thereby handling the request itself.

When delegating from one middleware to another in a sequence, one approach for dispatching systems is to use an intermediary request handler composing the middleware sequence as a way to link middleware together. The final or innermost middleware will act as a gateway to application code and generate a response from its results; alternately, the middleware MAY delegate this responsibility to a dedicated request handler.

Why doesn't middleware use __invoke?

Doing so would conflict with existing middleware that implements the double-pass approach and may want to implement the middleware interface for purposes of forward compatibility with this specification.

Why the name process()?

We reviewed a number of existing monolithic and middleware frameworks to determine what method(s) each defined for processing incoming requests. We found the following were commonly used:

  • __invoke (within middleware systems, such as Slim, Expressive, Relay, etc.)
  • handle (in particular, software derived from Symfony's HttpKernel)
  • dispatch (Zend Framework's DispatchableInterface)

We chose to allow a forward-compatible approach for such classes to repurpose themselves as middleware (or middleware compatible with this specification), and thus needed to choose a name not in common usage. As such, we chose process, to indicate processing a request.

Why is a server request required?

To make it clear that the middleware can only be used in a synchronous, server side context.

While not all middleware will need to use the additional methods defined by the server request interface, outbound requests are typically processed asynchronously and would typically return a promise of a response. (This is primarily due to the fact that multiple requests can be made in parallel and processed as they are returned.) It is outside the scope of this proposal to address the needs of asynchronous request/response life cycles.

Attempting to define client middleware would be premature at this point. Any future proposal that is focused on client side request processing should have the opportunity to define a standard that is specific to the nature of asynchronous middleware.

See "client vs server side middleware" in relevant links for additional information.

What is the role of the request handler?

Middleware has the following roles:

  • Producing a response on its own. If specific request conditions are met, the middleware can produce and return a response.

  • Returning the result of the request handler. In cases where the middleware cannot produce its own response, it can delegate to the request handler to produce one; sometimes this may involve providing a transformed request (e.g., to inject a request attribute, or the results of parsing the request body).

  • Manipulating and returning the response produced by the request handler. In some cases, the middleware may be interested in manipulating the response the request handler returns (e.g., to gzip the response body, to add CORS headers, etc.). In such cases, the middleware will capture the response returned by the request handler, and return a transformed response on completion.

In these latter two cases, the middleware may have code such as the following:

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

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

How the handler acts is entirely up to the developer, so long as it produces a response.

In one common scenario, the handler implements a queue or a stack of middleware instances internally. In such cases, calling $handler->handle($request) will advance the internal pointer, pull the middleware associated with that pointer, and call it using $middleware->process($request, $this). If no more middleware exists, it will generally either raise an exception or return a canned response.

Another possibility is for routing middleware that matches the incoming server request to a specific handler, and then returns the response generated by executing that handler. If unable to route to a handler, it would instead execute the handler provided to the middleware. (This sort of mechanism can even be used in conjunction with middleware queues and stacks.)

6.3 Example Interface Interactions

The two interfaces, RequestHandlerInterface and MiddlewareInterface, were designed to work in conjunction with one another. Middleware gains flexibility when de-coupled from any over-arching application layer, and instead only relying on the provided request handler to produce a response.

Two approaches to middleware dispatch systems that the Working Group observed and/or implemented are demonstrated below. Additionally, examples of re-usable middleware are provided to demonstrate how to write middleware that is loosely-coupled.

Please note that these are not suggested as definitive or exclusive approaches to defining middleware dispatch systems.

Queue-based request handler

In this approach, a request handler maintains a queue of middleware, and a fallback response to return if the queue is exhausted without returning a response. When executing the first middleware, the queue passes itself as a request handler to the middleware.

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

An application bootstrap might then look like 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());

This system has two request handlers: one that will produce a response if the last middleware delegates to the request handler, and one for dispatching the middleware layers. (In this example, the RoutingMiddleware will likely execute composed handlers on a successful route match; see more on that below.)

This approach has the following benefits:

  • Middleware does not need to know anything about any other middleware or how it is composed in the application.
  • The QueueRequestHandler is agnostic of the PSR-7 implementation in use.
  • Middleware is executed in the order it is added to the application, making the code explicit.
  • Generation of the "fallback" response is delegated to the application developer. This allows the developer to determine whether that should be a "404 Not Found" condition, a default page, etc.

Decoration-based request handler

In this approach, a request handler implementation decorates both a middleware instance and a fallback request handler to pass to it. The application is built from the outside-in, passing each request handler "layer" to the next outer one.

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

Similar to the queue-based middleware, request handlers serve two purposes in this system:

  • Producing a fallback response if no other layer does.
  • Dispatching middleware.

Reusable Middleware Examples

在上面的例子中,我们在每个例子都有两个中间件。为了让它们在任何一种情况下都能工作,我们需要编写它们,使它们能够适当地交互。

努力实现最大互操作性的中间件的实现者可能需要考虑以下准则:

  • 测试一个要求条件的请求。如果不满足条件,则使用一个原型响应或一个复合响应工厂来生成和回复一个响应。

  • 如果满足前提条件,则将响应的创建委托给所提供的请求处理程序,也可以通过操作所提供的请求来提供“新”请求。 (e.g., $handler->handle($request->withAttribute('foo', 'bar')).

  • 或者不改变地传递请求处理程序返回的响应,或者通过操纵返回的响应来提供新的响应(e.g., return $response->withHeader('X-Foo-Bar', 'baz')).

 AuthorizationMiddleware 授权中间件将执行以下三个指导原则:

  • 如果需要授权,但请求未经授权,则它将使用组合原型响应生成“unauthorized”响应。.
  • 如果不需要授权,它会将请求委托给处理程序而不做任何更改。
  • 如果需要授权并且请求被授权,它将把请求委托给处理程序,并根据请求对返回的响应进行签名。
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"路由中间件实现遵循一个类似的过程:它分析请求以查看它是否匹配已知的路由。在这个特定的实现中,路由映射到请求处理程序,中间件本质上委托给它们以生成响应。但是,在没有匹配路由的情况下,它将执行传递给它的处理程序,以生成要返回的响应。

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);
    }
}
chenlian 翻译于 4年前

7. People

This PSR was produced by a FIG Working Group with the following members:

The working group would also like to acknowledge the contributions of:

8. Votes

9. Relevant Links

Note: Order descending chronologically.

10. Errata

...

本文章首发在 LearnKu.com 网站上。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

贡献者:6
讨论数量: 0
发起讨论 只看当前版本


暂无话题~