PHP PSR-18:构建可互换的HTTP客户端与重试机制
大家好,今天我们来聊聊如何利用PHP的PSR-18(HTTP Client)来构建可互换的HTTP客户端以及实现重试机制。PSR-18的出现,为PHP的HTTP客户端开发带来了标准化,使得我们可以轻松地切换不同的HTTP客户端实现,而无需修改大量的业务代码。同时,结合重试机制,可以有效地提高应用的健壮性,应对网络波动等异常情况。
1. PSR-18:HTTP客户端接口的定义
PSR-18定义了一组接口,用于发送HTTP请求并接收响应。它的核心接口是 PsrHttpClientClientInterface,该接口定义了一个方法 sendRequest(RequestInterface $request): ResponseInterface。
RequestInterface:代表一个HTTP请求,通常由PsrHttpMessageRequestInterface实现。ResponseInterface:代表一个HTTP响应,通常由PsrHttpMessageResponseInterface实现。
简单来说,我们需要实现 ClientInterface 接口,并使用 RequestInterface 对象构建HTTP请求,然后通过 sendRequest 方法发送请求并获得 ResponseInterface 对象。
2. 选择PSR-18的实现:Guzzle、Symfony HttpClient等
目前,有很多实现了PSR-18的HTTP客户端库,其中比较流行的包括:
- Guzzle: 可能是PHP世界中最流行的HTTP客户端,功能强大,灵活。
- Symfony HttpClient: Symfony框架提供的HTTP客户端,性能优秀,易于集成。
- Buzz: 另一个流行的HTTP客户端,简单易用。
选择哪个实现取决于项目的具体需求。例如,如果项目已经使用了Symfony框架,那么Symfony HttpClient是一个不错的选择。 如果追求更灵活的配置,Guzzle可能更适合。
3. 使用PSR-18发送HTTP请求
让我们以Guzzle为例,展示如何使用PSR-18发送HTTP请求:
首先,安装Guzzle:
composer require guzzlehttp/guzzle
然后,编写代码:
<?php
require 'vendor/autoload.php';
use GuzzleHttpClient;
use GuzzleHttpPsr7Request;
use PsrHttpClientClientExceptionInterface;
try {
$client = new Client();
$request = new Request('GET', 'https://api.example.com/data');
$response = $client->sendRequest($request);
echo 'Status Code: ' . $response->getStatusCode() . PHP_EOL;
echo 'Body: ' . $response->getBody() . PHP_EOL;
} catch (ClientExceptionInterface $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
这段代码首先创建了一个Guzzle客户端实例。然后,创建了一个GET请求,目标URL是 https://api.example.com/data。 sendRequest 方法发送请求,并返回一个 ResponseInterface 对象。 最后,我们从响应对象中获取状态码和响应体。 使用 try-catch 块来捕获可能发生的异常。
4. 构建可互换的HTTP客户端
PSR-18的优势在于可以轻松地切换不同的HTTP客户端实现。我们可以创建一个抽象层,将业务代码与具体的HTTP客户端实现解耦。
首先,定义一个接口:
<?php
namespace AppHttpClient;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpClientClientExceptionInterface;
interface HttpClientInterface
{
/**
* Sends a PSR-7 request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws ClientExceptionInterface If an error happens while processing the request.
*/
public function sendRequest(RequestInterface $request): ResponseInterface;
}
然后,创建Guzzle的实现:
<?php
namespace AppHttpClient;
use GuzzleHttpClient;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpClientClientExceptionInterface;
class GuzzleHttpClient implements HttpClientInterface
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
try {
return $this->client->sendRequest($request);
} catch (ClientExceptionInterface $e) {
throw $e;
}
}
}
再创建一个Symfony HttpClient的实现:
<?php
namespace AppHttpClient;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpClientClientExceptionInterface;
use SymfonyComponentHttpClientHttpClient;
use SymfonyContractsHttpClientHttpClientInterface as SymfonyHttpClientInterface;
use SymfonyContractsHttpClientExceptionTransportExceptionInterface;
use SymfonyContractsHttpClientExceptionClientExceptionInterface as SymfonyClientExceptionInterface;
use SymfonyContractsHttpClientExceptionServerExceptionInterface as SymfonyServerExceptionInterface;
class SymfonyHttpClient implements HttpClientInterface
{
private SymfonyHttpClientInterface $client;
public function __construct(SymfonyHttpClientInterface $client)
{
$this->client = $client;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
try {
$response = $this->client->request(
$request->getMethod(),
(string) $request->getUri(),
[
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
'http_version' => $request->getProtocolVersion(),
]
);
// Convert Symfony Response to PSR-7 Response
$psrResponse = new GuzzleHttpPsr7Response(
$response->getStatusCode(),
$response->getHeaders(),
$response->getContent(),
$response->getProtocolVersion()
);
return $psrResponse;
} catch (TransportExceptionInterface | SymfonyClientExceptionInterface | SymfonyServerExceptionInterface $e) {
throw new PsrHttpClientClientException($e->getMessage(), 0, $e);
}
}
}
注意:Symfony HttpClient 的 request 方法与 PSR-18 的 sendRequest 方法的参数形式不同,需要进行适配。 此外,Symfony HttpClient返回的Response对象不是PSR-7标准的,因此需要转换。 这里我们使用了 GuzzleHttpPsr7Response 进行转换。
现在,我们可以使用 HttpClientInterface 接口来发送HTTP请求,而无需关心具体的实现:
<?php
namespace AppService;
use AppHttpClientHttpClientInterface;
use PsrHttpMessageRequestFactoryInterface;
use PsrHttpMessageStreamFactoryInterface;
class ApiService
{
private HttpClientInterface $httpClient;
private RequestFactoryInterface $requestFactory;
private StreamFactoryInterface $streamFactory;
public function __construct(HttpClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory)
{
$this->httpClient = $httpClient;
$this->requestFactory = $requestFactory;
$this->streamFactory = $streamFactory;
}
public function fetchData(string $url): string
{
$request = $this->requestFactory->createRequest('GET', $url);
try {
$response = $this->httpClient->sendRequest($request);
return $response->getBody()->getContents();
} catch (PsrHttpClientClientExceptionInterface $e) {
// Handle exception
return 'Error: ' . $e->getMessage();
}
}
}
在这个例子中,ApiService 依赖于 HttpClientInterface 接口,而不是具体的HTTP客户端实现。 我们可以通过依赖注入的方式,将 GuzzleHttpClient 或 SymfonyHttpClient 注入到 ApiService 中。
<?php
require 'vendor/autoload.php';
use AppServiceApiService;
use AppHttpClientGuzzleHttpClient;
use GuzzleHttpClient;
use NyholmPsr7FactoryPsr17Factory;
// Instantiate Guzzle client
$guzzleClient = new Client();
$psr17Factory = new Psr17Factory();
// Inject GuzzleHttpClient into ApiService
$apiService = new ApiService(new GuzzleHttpClient($guzzleClient), $psr17Factory, $psr17Factory);
$data = $apiService->fetchData('https://api.example.com/data');
echo $data . PHP_EOL;
// To switch to Symfony HttpClient, simply change the instantiation:
// use AppHttpClientSymfonyHttpClient;
// use SymfonyComponentHttpClientHttpClient;
// $symfonyClient = HttpClient::create();
// $apiService = new ApiService(new SymfonyHttpClient($symfonyClient), $psr17Factory, $psr17Factory);
// $data = $apiService->fetchData('https://api.example.com/data');
// echo $data . PHP_EOL;
5. 实现重试机制
在实际应用中,网络波动等原因可能导致HTTP请求失败。为了提高应用的健壮性,我们可以实现重试机制。
我们可以使用装饰器模式来实现重试机制。 首先,创建一个重试客户端:
<?php
namespace AppHttpClient;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpClientClientExceptionInterface;
class RetryHttpClient implements HttpClientInterface
{
private HttpClientInterface $httpClient;
private int $maxRetries;
private int $delay;
public function __construct(HttpClientInterface $httpClient, int $maxRetries = 3, int $delay = 1000)
{
$this->httpClient = $httpClient;
$this->maxRetries = $maxRetries; // 最大重试次数
$this->delay = $delay; // 重试间隔,单位毫秒
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$attempt = 0;
while ($attempt < $this->maxRetries) {
try {
return $this->httpClient->sendRequest($request);
} catch (ClientExceptionInterface $e) {
$attempt++;
if ($attempt >= $this->maxRetries) {
throw $e; // 达到最大重试次数,抛出异常
}
usleep($this->delay * 1000); // 延迟重试
}
}
// This should never be reached, but it's good to have a fallback.
throw new RuntimeException("RetryHttpClient failed after {$this->maxRetries} attempts.");
}
}
在这个例子中,RetryHttpClient 接收一个 HttpClientInterface 实例作为参数,并实现了 sendRequest 方法。 在 sendRequest 方法中,我们使用一个循环来重试请求。 如果请求成功,则返回响应。 如果请求失败,则增加重试次数,并延迟一段时间后重试。 如果达到最大重试次数,则抛出异常。
现在,我们可以将 RetryHttpClient 装饰到现有的HTTP客户端上:
<?php
require 'vendor/autoload.php';
use AppServiceApiService;
use AppHttpClientGuzzleHttpClient;
use AppHttpClientRetryHttpClient;
use GuzzleHttpClient;
use NyholmPsr7FactoryPsr17Factory;
// Instantiate Guzzle client
$guzzleClient = new Client();
$psr17Factory = new Psr17Factory();
// Decorate GuzzleHttpClient with RetryHttpClient
$retryHttpClient = new RetryHttpClient(new GuzzleHttpClient($guzzleClient), 3, 500);
// Inject RetryHttpClient into ApiService
$apiService = new ApiService($retryHttpClient, $psr17Factory, $psr17Factory);
$data = $apiService->fetchData('https://api.example.com/data');
echo $data . PHP_EOL;
通过这种方式,我们可以为任何实现了 HttpClientInterface 的HTTP客户端添加重试机制,而无需修改其内部代码。
6. 更精细的重试策略
上面的重试机制比较简单,每次失败后都会重试。 在实际应用中,我们可能需要更精细的重试策略,例如:
- 根据状态码重试: 只对特定状态码(例如500、502、503)的响应进行重试。
- 指数退避: 每次重试都增加延迟时间,避免对服务器造成过大的压力。
- 最大重试时间: 限制总的重试时间,避免无限重试。
我们可以通过修改 RetryHttpClient 来实现更精细的重试策略。
以下是一个根据状态码重试的例子:
<?php
namespace AppHttpClient;
use PsrHttpMessageRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpClientClientExceptionInterface;
class StatusCodeRetryHttpClient implements HttpClientInterface
{
private HttpClientInterface $httpClient;
private int $maxRetries;
private int $delay;
private array $retryStatusCodes;
public function __construct(HttpClientInterface $httpClient, int $maxRetries = 3, int $delay = 1000, array $retryStatusCodes = [500, 502, 503])
{
$this->httpClient = $httpClient;
$this->maxRetries = $maxRetries;
$this->delay = $delay;
$this->retryStatusCodes = $retryStatusCodes;
}
public function sendRequest(RequestInterface $request): ResponseInterface
{
$attempt = 0;
while ($attempt < $this->maxRetries) {
try {
$response = $this->httpClient->sendRequest($request);
if (in_array($response->getStatusCode(), $this->retryStatusCodes)) {
$attempt++;
if ($attempt >= $this->maxRetries) {
return $response; // 达到最大重试次数,返回最后的响应(虽然是错误码)
}
usleep($this->delay * 1000); // 延迟重试
} else {
return $response; // 请求成功,返回响应
}
} catch (ClientExceptionInterface $e) {
$attempt++;
if ($attempt >= $this->maxRetries) {
throw $e; // 达到最大重试次数,抛出异常
}
usleep($this->delay * 1000); // 延迟重试
}
}
// This should never be reached, but it's good to have a fallback.
throw new RuntimeException("RetryHttpClient failed after {$this->maxRetries} attempts.");
}
}
在这个例子中,我们添加了一个 $retryStatusCodes 属性,用于指定需要重试的状态码。 在 sendRequest 方法中,我们检查响应的状态码是否在 $retryStatusCodes 列表中,如果是,则进行重试。
7. 总结:标准化、可互换、更健壮
通过使用PHP PSR-18,我们可以构建可互换的HTTP客户端,从而简化了HTTP客户端的切换和维护。 结合重试机制,可以有效地提高应用的健壮性,应对网络波动等异常情况。 通过装饰器模式,可以为HTTP客户端添加各种额外的功能,例如重试、日志记录、性能监控等,而无需修改其内部代码。 记住,选择合适的HTTP客户端实现和重试策略取决于项目的具体需求,需要根据实际情况进行调整。