PHP-FIG Standards扩展开发:HTTP客户端PSR-18实现指南

【免费下载链接】fig-standards Standards either proposed or approved by the Framework Interop Group 【免费下载链接】fig-standards 项目地址: https://gitcode.com/gh_mirrors/fi/fig-standards

你是否在开发PHP应用时遇到过HTTP客户端兼容性问题?不同客户端库的API差异是否让你头疼?本文将带你从零开始实现符合PSR-18标准的HTTP客户端,解决跨库兼容难题,让你的代码更具通用性和可维护性。读完本文,你将掌握PSR-18核心接口设计、异常处理机制和完整实现步骤,轻松应对各种HTTP请求场景。

为什么需要PSR-18标准

在现代PHP开发中,HTTP客户端是连接各种服务的重要组件。然而,不同客户端库(如Guzzle、Symfony HttpClient等)的API设计差异导致代码耦合严重,切换客户端时需要大量修改代码。PHP-FIG(PHP Framework Interop Group)制定的PSR-18标准(accepted/PSR-18-http-client.md)定义了HTTP客户端的通用接口,实现了"一次编写,到处运行"的目标。

PSR-18标准的核心价值在于:

  • 解耦:业务代码不再依赖具体客户端实现
  • 可替换:随时切换不同HTTP客户端库,无需修改业务逻辑
  • 可测试:轻松编写模拟客户端进行单元测试
  • 标准化:统一的异常处理和返回值格式

PSR-18核心接口解析

ClientInterface:客户端核心接口

PSR-18的核心是ClientInterface,它只定义了一个方法sendRequest(),接收PSR-7请求对象并返回PSR-7响应对象。

namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

interface ClientInterface
{
    /**
     * Sends a PSR-7 request and returns a PSR-7 response.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
     */
    public function sendRequest(RequestInterface $request): ResponseInterface;
}

这个简洁的接口定义了HTTP客户端的最基本能力。所有符合PSR-18标准的客户端都必须实现这个接口,确保了接口一致性。

异常处理体系

PSR-18定义了完善的异常处理机制,所有异常都实现ClientExceptionInterface接口,形成了清晰的异常层次结构:

mermaid

  • ClientExceptionInterface:所有客户端异常的根接口
  • RequestExceptionInterface:请求格式错误时抛出,如缺少必要头信息
  • NetworkExceptionInterface:网络问题时抛出,如连接失败、超时等

客户端必须在无法发送请求或无法解析响应时抛出相应异常,而4xx/5xx等HTTP错误状态码不应抛出异常,应作为正常响应返回(accepted/PSR-18-http-client.md#error-handling)。

实现步骤:从零构建PSR-18客户端

1. 准备工作

首先,确保项目中已安装PSR-7和PSR-18相关依赖:

composer require psr/http-message psr/http-client

你还需要一个PSR-7实现,如nyholm/psr7

composer require nyholm/psr7

2. 实现ClientInterface

创建MyHttpClient类并实现ClientInterface

<?php

namespace MyApp\Http;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;

class MyHttpClient implements ClientInterface
{
    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        // 实现发送请求的逻辑
        $this->validateRequest($request);
        
        try {
            $response = $this->doSendRequest($request);
            return $response;
        } catch (RequestExceptionInterface $e) {
            throw $e;
        } catch (NetworkExceptionInterface $e) {
            throw $e;
        } catch (\Exception $e) {
            throw new \MyApp\Exceptions\GenericClientException(
                'Failed to send request: ' . $e->getMessage(),
                $e->getCode(),
                $e,
                $request
            );
        }
    }
    
    private function validateRequest(RequestInterface $request): void
    {
        // 验证请求是否有效
        if (!$request->getUri()->getHost()) {
            throw new \MyApp\Exceptions\InvalidRequestException(
                'Request URI must contain a host',
                0,
                null,
                $request
            );
        }
        
        if (!$request->getMethod()) {
            throw new \MyApp\Exceptions\InvalidRequestException(
                'Request must have a method',
                0,
                null,
                $request
            );
        }
    }
    
    private function doSendRequest(RequestInterface $request): ResponseInterface
    {
        // 实际发送请求的底层实现
        $ch = curl_init();
        
        // 设置CURL选项
        curl_setopt($ch, CURLOPT_URL, (string)$request->getUri());
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request->getMethod());
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        
        // 处理请求头
        $headers = [];
        foreach ($request->getHeaders() as $name => $values) {
            $headers[] = $name . ': ' . implode(', ', $values);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        
        // 处理请求体
        $body = $request->getBody();
        if ($body->isReadable() && $body->getSize() > 0) {
            $body->rewind();
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body->getContents());
        }
        
        // 执行请求
        $responseBody = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        $errorCode = curl_errno($ch);
        
        curl_close($ch);
        
        if ($errorCode !== CURLE_OK) {
            if (in_array($errorCode, [CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEOUTED])) {
                throw new \MyApp\Exceptions\NetworkException(
                    'Network error: ' . $error,
                    $errorCode,
                    null,
                    $request
                );
            } else {
                throw new \MyApp\Exceptions\RequestException(
                    'Request error: ' . $error,
                    $errorCode,
                    null,
                    $request
                );
            }
        }
        
        // 构建PSR-7响应对象
        $responseFactory = new \Nyholm\Psr7\Factory\Psr17Factory();
        $response = $responseFactory->createResponse($httpCode)
            ->withBody($responseFactory->createStream($responseBody));
            
        return $response;
    }
}

3. 实现异常类

创建符合PSR-18规范的异常类:

<?php

namespace MyApp\Exceptions;

use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;

class InvalidRequestException extends \RuntimeException implements RequestExceptionInterface
{
    private $request;
    
    public function __construct(string $message, int $code = 0, \Throwable $previous = null, RequestInterface $request)
    {
        parent::__construct($message, $code, $previous);
        $this->request = $request;
    }
    
    public function getRequest(): RequestInterface
    {
        return $this->request;
    }
}
<?php

namespace MyApp\Exceptions;

use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;

class NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
    private $request;
    
    public function __construct(string $message, int $code = 0, \Throwable $previous = null, RequestInterface $request)
    {
        parent::__construct($message, $code, $previous);
        $this->request = $request;
    }
    
    public function getRequest(): RequestInterface
    {
        return $this->request;
    }
}

客户端使用示例

实现了符合PSR-18标准的客户端后,可以这样使用:

<?php

// 创建请求
$requestFactory = new \Nyholm\Psr7\Factory\Psr17Factory();
$request = $requestFactory->createRequest('GET', 'https://api.example.com/data')
    ->withHeader('Accept', 'application/json');

// 使用自定义客户端
$client = new \MyApp\Http\MyHttpClient();

try {
    $response = $client->sendRequest($request);
    
    // 处理响应
    if ($response->getStatusCode() === 200) {
        $data = json_decode((string)$response->getBody(), true);
        // 处理数据
    } else {
        // 处理非200状态码
        error_log('Request failed with status code: ' . $response->getStatusCode());
    }
} catch (\Psr\Http\Client\RequestExceptionInterface $e) {
    // 处理请求格式错误
    error_log('Invalid request: ' . $e->getMessage());
} catch (\Psr\Http\Client\NetworkExceptionInterface $e) {
    // 处理网络错误
    error_log('Network error: ' . $e->getMessage());
} catch (\Psr\Http\Client\ClientExceptionInterface $e) {
    // 处理其他客户端错误
    error_log('Client error: ' . $e->getMessage());
}

测试与验证

为确保客户端符合PSR-18标准,建议使用PHP-FIG提供的验证工具:

composer require --dev php-fig/http-client-implementation-test

创建测试类:

<?php

namespace MyApp\Tests;

use PHPUnit\Framework\TestCase;
use MyApp\Http\MyHttpClient;
use Nyholm\Psr7\Factory\Psr17Factory;

class MyHttpClientTest extends TestCase
{
    public function testSendRequest()
    {
        $client = new MyHttpClient();
        $requestFactory = new Psr17Factory();
        
        $request = $requestFactory->createRequest('GET', 'https://httpbin.org/get');
        $response = $client->sendRequest($request);
        
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertJson((string)$response->getBody());
    }
    
    public function testInvalidRequest()
    {
        $this->expectException(\Psr\Http\Client\RequestExceptionInterface::class);
        
        $client = new MyHttpClient();
        $requestFactory = new Psr17Factory();
        
        // 创建一个无效请求(没有主机名)
        $uri = $requestFactory->createUri('/path-without-host');
        $request = $requestFactory->createRequest('GET', $uri);
        
        $client->sendRequest($request);
    }
}

总结与最佳实践

实现符合PSR-18标准的HTTP客户端需要注意以下几点:

  1. 严格遵循接口定义:确保sendRequest()方法签名正确,返回类型和异常符合规范
  2. 完善的异常处理:准确区分请求错误和网络错误,提供足够的调试信息
  3. 请求验证:发送前验证请求的有效性,如必须包含主机名和HTTP方法
  4. PSR-7兼容性:确保与任何PSR-7实现(请求/响应)兼容
  5. 测试覆盖:编写全面的单元测试,包括正常情况和各种异常场景

通过遵循PSR-18标准,你的HTTP客户端实现将具有良好的互操作性和可维护性,为项目的长期发展奠定坚实基础。PHP-FIG的其他标准(如PSR-7PSR-17)也值得深入学习,它们共同构成了现代PHP应用的标准生态系统。

扩展学习资源

希望本文能帮助你顺利实现符合PSR-18标准的HTTP客户端。如有任何问题或建议,欢迎参与PHP-FIG社区讨论,共同推动PHP生态系统的标准化发展。记得点赞、收藏、关注,下期我们将探讨PSR-18客户端的高级特性和性能优化技巧!

【免费下载链接】fig-standards Standards either proposed or approved by the Framework Interop Group 【免费下载链接】fig-standards 项目地址: https://gitcode.com/gh_mirrors/fi/fig-standards

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐