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:RequestInterface和ResponseInterface的父接口,定义了 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。 |
中间件的工作流程:
- 接收一个
ServerRequestInterface对象。 - 执行一些逻辑(例如,身份验证、日志记录)。
- 可以选择修改请求对象并创建一个新的
ServerRequestInterface实例。 - 调用
$handler->handle($request)将请求传递给下一个中间件或最终的请求处理器。 - 接收一个
ResponseInterface对象。 - 执行一些逻辑(例如,添加头部、记录响应)。
- 可以选择修改响应对象并创建一个新的
ResponseInterface实例。 - 返回
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);
解释:
- 我们使用
ServerRequestFactory::fromGlobals()创建一个ServerRequestInterface实例,它从全局变量($_GET,$_POST,$_SERVER等)中提取数据。 - 我们创建了一个
HelloHandler实例,它负责处理请求并返回一个 JSON 响应。 - 我们创建了一个
LoggingMiddleware实例,它负责记录请求和响应信息。 - 我们创建了一个
MiddlewareQueue实例,并将HelloHandler作为最终的请求处理器传递给它。 - 我们将
LoggingMiddleware添加到中间件队列中。 - 我们调用
$middlewareQueue->handle($request)处理请求。MiddlewareQueue会按照添加的顺序依次调用中间件,并将请求传递给最终的请求处理器。 - 我们使用
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)来构建高性能的中间件架构。
- 路由中间件: 将路由逻辑集成到中间件管道中,从而实现更灵活的路由机制。
- 依赖注入容器集成: 将依赖注入容器与中间件管道集成,从而更好地管理中间件的依赖项。