PHP PSR-7/15/17的应用:构建基于HTTP Message的中间件架构

PHP PSR-7/15/17 应用:构建基于 HTTP Message 的中间件架构

大家好,今天我们来深入探讨 PHP PSR-7/15/17 标准在构建基于 HTTP Message 的中间件架构中的应用。这将是一场理论与实践相结合的旅程,我们将从概念入手,最终构建一个简单的中间件管道。

PSR-7:HTTP Message 接口

PSR-7 (HTTP Message Interfaces) 定义了 HTTP 请求和响应消息的通用接口。它的核心目标是提供一套标准,让不同的 HTTP 组件(如服务器、客户端、中间件)能够以统一的方式处理 HTTP 消息,从而提高代码的可移植性和互操作性。

核心接口:

  • PsrHttpMessageRequestInterface: 代表 HTTP 请求消息。
  • PsrHttpMessageResponseInterface: 代表 HTTP 响应消息。
  • PsrHttpMessageMessageInterface: RequestInterfaceResponseInterface 的父接口,定义了 HTTP 消息的通用属性和方法,例如获取和设置头部、正文等。
  • PsrHttpMessageStreamInterface: 代表 HTTP 消息的正文,允许以流的方式读取和写入数据。
  • PsrHttpMessageUriInterface: 代表 URI (Uniform Resource Identifier)。

接口方法示例 (RequestInterface):

方法 描述
getMethod(): string 获取 HTTP 请求方法 (GET, POST, PUT, DELETE 等)。
withMethod(string $method): RequestInterface 返回一个新的 RequestInterface 实例,使用给定的 HTTP 方法。 注意:PSR-7 对象是不可变的,每次修改都会返回一个新的实例。
getUri(): UriInterface 获取请求的 URI。
withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface 返回一个新的 RequestInterface 实例,使用给定的 URI。$preserveHost 控制是否保留原有的 Host 头部。
getHeaders(): array 获取所有 HTTP 头部,以关联数组的形式返回 (header name => array of values)。
getHeaderLine(string $name): string 获取指定 HTTP 头部的值,将所有值连接成一个字符串,用逗号分隔。
withHeader(string $name, $value): RequestInterface 返回一个新的 RequestInterface 实例,添加或替换指定的 HTTP 头部。$value 可以是字符串或字符串数组。
withoutHeader(string $name): RequestInterface 返回一个新的 RequestInterface 实例,移除指定的 HTTP 头部。
getBody(): StreamInterface 获取请求的正文。
withBody(StreamInterface $body): RequestInterface 返回一个新的 RequestInterface 实例,使用给定的正文。
getRequestTarget(): string 获取请求目标(Request Target),通常是 URI 的路径部分,但也可能是 *authority-form
withRequestTarget(string $requestTarget): RequestInterface 返回一个新的 RequestInterface 实例,使用给定的请求目标。

代码示例 (使用 PSR-7 创建一个简单的响应):

<?php

use PsrHttpMessageResponseInterface;
use PsrHttpMessageStreamInterface;
use LaminasDiactorosResponse; // 或者使用其他 PSR-7 实现,如 Guzzle

// 创建一个 StreamInterface 实例 (正文)
$body = fopen('php://temp', 'r+');
fwrite($body, 'Hello, PSR-7!');
rewind($body); // 确保指针回到开头

/** @var StreamInterface $stream */
$stream = new LaminasDiactorosStream($body);

// 创建一个 ResponseInterface 实例
/** @var ResponseInterface $response */
$response = new Response();
$response = $response->withStatus(200)
                     ->withHeader('Content-Type', 'text/plain')
                     ->withBody($stream);

// 输出响应
echo $response->getStatusCode() . ' ' . $response->getReasonPhrase() . "n";
foreach ($response->getHeaders() as $name => $values) {
    echo $name . ': ' . implode(', ', $values) . "n";
}
echo $response->getBody();

fclose($body); // 关闭资源

关键点:

  • 不可变性: PSR-7 对象是不可变的。 任何修改操作(例如 withHeader())都会返回一个新的对象实例,而不是修改原始对象。 这有助于避免意外的副作用,并使代码更易于推理。
  • StreamInterface: 使用 StreamInterface 处理 HTTP 消息正文,允许以流的方式读取和写入数据,这对于处理大型文件或实时数据非常有用。
  • 实现: PSR-7 只是接口定义,需要使用具体的实现库,例如 Guzzle HTTP Client, Laminas Diactoros, Nyholm/psr7 等。

PSR-15:HTTP 处理程序

PSR-15 (HTTP Handlers) 定义了处理 HTTP 请求的接口。它引入了两个关键接口:

  • PsrHttpServerRequestHandlerInterface: 负责处理一个 HTTP 请求并生成一个 HTTP 响应。
  • PsrHttpServerMiddlewareInterface: 允许你在请求和响应之间插入逻辑,例如身份验证、日志记录、请求转换等。

接口方法:

接口 方法 描述
RequestHandlerInterface handle(ServerRequestInterface $request): ResponseInterface 处理给定的请求并返回一个响应。
MiddlewareInterface process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 处理给定的请求,可以选择调用 $handler->handle($request) 将请求传递给下一个中间件或最终的请求处理器。 如果中间件自己生成了响应,则可以直接返回,不再调用 $handler

中间件的工作流程:

  1. 接收一个 ServerRequestInterface 对象。
  2. 执行一些逻辑(例如,身份验证、日志记录)。
  3. 可以选择修改请求对象并创建一个新的 ServerRequestInterface 实例。
  4. 调用 $handler->handle($request) 将请求传递给下一个中间件或最终的请求处理器。
  5. 接收一个 ResponseInterface 对象。
  6. 执行一些逻辑(例如,添加头部、记录响应)。
  7. 可以选择修改响应对象并创建一个新的 ResponseInterface 实例。
  8. 返回 ResponseInterface 对象。

代码示例 (一个简单的日志中间件):

<?php

namespace AppMiddleware;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerMiddlewareInterface;
use PsrHttpServerRequestHandlerInterface;
use PsrLogLoggerInterface; // 假设你使用 PSR-3 日志接口

class LoggingMiddleware implements MiddlewareInterface
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $this->logger->info('Incoming request', [
            'method' => $request->getMethod(),
            'uri' => (string) $request->getUri()
        ]);

        $response = $handler->handle($request);

        $this->logger->info('Outgoing response', [
            'status' => $response->getStatusCode()
        ]);

        return $response;
    }
}

代码示例 (一个简单的请求处理器):

<?php

namespace AppHandler;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerRequestHandlerInterface;
use LaminasDiactorosResponseJsonResponse; // 或者使用其他 PSR-7 实现

class HelloHandler implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $name = $request->getQueryParams()['name'] ?? 'World';
        return new JsonResponse(['message' => 'Hello, ' . $name . '!']);
    }
}

关键点:

  • 分离关注点: 中间件允许你将不同的功能(例如,身份验证、日志记录、缓存)分离到独立的组件中,从而使代码更易于维护和测试。
  • 可重用性: 中间件可以被多个应用程序重用,从而提高代码的效率。
  • 灵活性: 中间件可以动态地添加到请求处理管道中,从而提供更大的灵活性。
  • 依赖注入: 中间件通常通过构造函数注入依赖项(例如,LoggerInterface),这使得它们更易于测试和配置。

PSR-17:HTTP 工厂

PSR-17 (HTTP Factories) 定义了创建 PSR-7 HTTP 消息对象的工厂接口。它的目标是消除对特定 PSR-7 实现的依赖,并允许你在不同的 HTTP 组件之间交换 HTTP 消息对象。

核心接口:

  • PsrHttpMessageRequestFactoryInterface: 创建 RequestInterface 对象。
  • PsrHttpMessageResponseFactoryInterface: 创建 ResponseInterface 对象。
  • PsrHttpMessageServerRequestFactoryInterface: 创建 ServerRequestInterface 对象。
  • PsrHttpMessageStreamFactoryInterface: 创建 StreamInterface 对象。
  • PsrHttpMessageUploadedFileFactoryInterface: 创建 UploadedFileInterface 对象。
  • PsrHttpMessageUriFactoryInterface: 创建 UriInterface 对象。

接口方法示例 (RequestFactoryInterface):

方法 描述
createRequest(string $method, UriInterface|string $uri): RequestInterface 创建一个新的 RequestInterface 实例,使用给定的 HTTP 方法和 URI。 URI 可以是字符串或 UriInterface 对象。

代码示例 (使用 PSR-17 创建一个请求):

<?php

use PsrHttpMessageRequestFactoryInterface;
use PsrHttpMessageRequestInterface;
use LaminasDiactorosRequestFactory; // 或者使用其他 PSR-17 实现

// 创建一个 RequestFactoryInterface 实例
/** @var RequestFactoryInterface $requestFactory */
$requestFactory = new RequestFactory();

// 创建一个 RequestInterface 实例
/** @var RequestInterface $request */
$request = $requestFactory->createRequest('GET', '/');

// 输出请求方法
echo $request->getMethod(); // 输出: GET

关键点:

  • 解耦: PSR-17 将创建 HTTP 消息对象的责任委托给工厂类,从而解耦了应用程序代码与具体的 PSR-7 实现。
  • 可配置性: 你可以使用不同的工厂类来创建不同类型的 HTTP 消息对象,从而提高应用程序的可配置性。
  • 测试: 你可以使用 mock 工厂类来测试应用程序代码,而无需依赖实际的 HTTP 服务器或客户端。

构建一个简单的中间件管道

现在,我们将结合 PSR-7/15/17 来构建一个简单的中间件管道。 我们将创建一个 MiddlewareQueue 类,它负责管理和执行中间件。

<?php

namespace AppMiddleware;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerMiddlewareInterface;
use PsrHttpServerRequestHandlerInterface;

class MiddlewareQueue implements RequestHandlerInterface
{
    /**
     * @var MiddlewareInterface[]
     */
    private array $middleware = [];

    private RequestHandlerInterface $finalHandler;

    public function __construct(RequestHandlerInterface $finalHandler)
    {
        $this->finalHandler = $finalHandler;
    }

    public function add(MiddlewareInterface $middleware): void
    {
        $this->middleware[] = $middleware;
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        // 创建一个匿名类,作为链式调用的核心
        $delegate = new class($this->middleware, $this->finalHandler) implements RequestHandlerInterface {
            /** @var MiddlewareInterface[] */
            private array $middleware;
            private RequestHandlerInterface $finalHandler;

            public function __construct(array $middleware, RequestHandlerInterface $finalHandler)
            {
                $this->middleware = $middleware;
                $this->finalHandler = $finalHandler;
            }

            public function handle(ServerRequestInterface $request): ResponseInterface
            {
                if (empty($this->middleware)) {
                    return $this->finalHandler->handle($request);
                }

                $middleware = array_shift($this->middleware);
                //注意这里,每次都传递一个新的匿名类实例,保证中间件处理的顺序
                return $middleware->process($request, new self($this->middleware, $this->finalHandler));
            }
        };

        return $delegate->handle($request);
    }
}

代码示例 (使用中间件管道):

<?php

use AppHandlerHelloHandler;
use AppMiddlewareLoggingMiddleware;
use AppMiddlewareMiddlewareQueue;
use LaminasDiactorosServerRequestFactory;
use LaminasHttpHandlerRunnerEmitterSapiEmitter;
use MonologHandlerStreamHandler;
use MonologLogger;

require_once __DIR__ . '/vendor/autoload.php';

// 1. 创建请求和响应对象
$request = ServerRequestFactory::fromGlobals();

// 2. 创建请求处理器
$helloHandler = new HelloHandler();

// 3. 创建中间件
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::INFO));
$loggingMiddleware = new LoggingMiddleware($logger);

// 4. 创建中间件队列
$middlewareQueue = new MiddlewareQueue($helloHandler);
$middlewareQueue->add($loggingMiddleware);

// 5. 处理请求
$response = $middlewareQueue->handle($request);

// 6. 发送响应
$emitter = new SapiEmitter();
$emitter->emit($response);

解释:

  1. 我们使用 ServerRequestFactory::fromGlobals() 创建一个 ServerRequestInterface 实例,它从全局变量($_GET, $_POST, $_SERVER 等)中提取数据。
  2. 我们创建了一个 HelloHandler 实例,它负责处理请求并返回一个 JSON 响应。
  3. 我们创建了一个 LoggingMiddleware 实例,它负责记录请求和响应信息。
  4. 我们创建了一个 MiddlewareQueue 实例,并将 HelloHandler 作为最终的请求处理器传递给它。
  5. 我们将 LoggingMiddleware 添加到中间件队列中。
  6. 我们调用 $middlewareQueue->handle($request) 处理请求。 MiddlewareQueue 会按照添加的顺序依次调用中间件,并将请求传递给最终的请求处理器。
  7. 我们使用 SapiEmitter 将响应发送到客户端。

优势:

  • 清晰的流程: 每个中间件只关注自己的特定任务,例如日志记录、身份验证等。
  • 易于扩展: 可以轻松地添加新的中间件来扩展应用程序的功能。
  • 可测试性: 可以单独测试每个中间件,从而提高代码的质量。

错误处理

在中间件架构中,错误处理是一个重要的方面。 你可以使用中间件来捕获异常并生成相应的错误响应。

代码示例 (一个简单的错误处理中间件):

<?php

namespace AppMiddleware;

use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use PsrHttpServerMiddlewareInterface;
use PsrHttpServerRequestHandlerInterface;
use LaminasDiactorosResponseJsonResponse;

class ErrorHandlingMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            return $handler->handle($request);
        } catch (Throwable $e) {
            // 记录错误信息
            error_log($e->getMessage() . "n" . $e->getTraceAsString());

            // 返回一个 JSON 错误响应
            return new JsonResponse([
                'error' => [
                    'message' => 'Internal Server Error',
                    'code' => 500
                ]
            ], 500);
        }
    }
}

使用:

ErrorHandlingMiddleware 添加到中间件队列的最前面,它可以捕获所有后续中间件和请求处理器抛出的异常。

总结

今天我们学习了 PSR-7/15/17 标准在构建基于 HTTP Message 的中间件架构中的应用。 PSR-7 提供了 HTTP 消息的通用接口,PSR-15 定义了处理 HTTP 请求的接口,PSR-17 定义了创建 HTTP 消息对象的工厂接口。 结合这些标准,我们可以构建灵活、可扩展和易于维护的应用程序。

未来方向

  • 异步中间件: 使用异步编程技术(例如,ReactPHP, Swoole)来构建高性能的中间件架构。
  • 路由中间件: 将路由逻辑集成到中间件管道中,从而实现更灵活的路由机制。
  • 依赖注入容器集成: 将依赖注入容器与中间件管道集成,从而更好地管理中间件的依赖项。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注