摆脱 PHP SDK 中的僵硬依赖。学习如何使用 PSR-7、PSR-17 和 PSR-18 标准以及 php-http/discovery,让用户使用他们喜欢的 HTTP 客户端,无论是 Guzzle、Symfony HttpClient 还是其他。这是 PHP 和 Symfony 开发者的必读文章。
这篇文章讨论了 SDK 开发者允许用户自带 HTTP 客户端的实用技巧,而不是强制使用他们未选择的客户端。它也针对希望在已支持的 SDK 中使用自己喜欢的 HTTP 客户端的用户。
你可能知道 PHP 生态系统中存在众多 HTTP 客户端的丛林:Guzzle、Symfony HttpClient、Buzz、CakePHP、HTTP 等。作为 SDK 维护者,支持所有这些包简直是个噩梦。幸运的是,有抽象层和技术可以帮助我们支持绝大多数客户端。让我们探索如何实现这一点!
首先,这里有一些 PSR,如果你不熟悉,可以将它们视为库开发者和你自己应用代码的流行技术推荐。这些 PSR 涵盖了许多主题,如编码风格、自动加载、缓存、容器、时钟,以及我们今天感兴趣的:HTTP。
[PSR-7: HTTP 消息] 这个描述了 Request 和 Response 的接口(以及一些“子类型”,如 Stream、UploadedFile 等),它被 PSR-18 用作 sendRequest 方法的参数类型和返回类型。
[PSR-18: HTTP 客户端] 这个 PSR 可以总结为:
一个用于发送 PSR-7 请求并返回 PSR-7 响应的通用接口。它提供了 ClientInterface 和 3 个异常接口来表示不同类型的失败。
[PSR-17: HTTP 工厂] 这个 PSR 为 PSR-7 中描述的所有 HTTP 消息接口提供了工厂。我们将在下面看到如何利用这些工厂。
这个包包含与 PSR-18 相关的代码(ClientInterface 和 3 个异常接口)。这正是我们需要类型化代码并避免依赖具体实现的东西。
这个既是代码库,也是 Composer 插件(从 v1.17 开始)。它负责 PSR-17 和 PSR-18 接口的具体实现的自动发现和自动安装。
它与接下来的 2 个包一起工作。
psr/http-client-implementation & psr/http-factory-implementation
这两个包有点特殊,因为它们是虚拟包。它们仅存在于 Packagist 注册表中,被“真实”包用于表示它们提供了符合 PSR-17 和/或 PSR-18 接口的具体实现。
关于虚拟包概念的快速说明:将其视为包级别的 PHP 接口。这里,在 SDK composer.json 中将 psr/http-client-implementation 虚拟包作为要求,表示 SDK 需要任何提供 PSR-18 具体实现的包。
由于 psr/http-client 直接依赖 psr/http-message,我们不需要依赖它来引用消息接口,如 RequestInterface、ResponseInterface 等。
最后一件有用的知识是 Composer 模式的 provide 属性。这里是一个具体用法:
"provide": {
"psr/http-client-implementation": "1.0",
},
provide 属性)。img
显示 HTTP 客户端如何与 PSR 协作的示意图
感谢 foo/sdk 要求的 php-http/http-discovery,两个应用程序的开发者只需要求他们喜欢的 HTTP 客户端和 foo/sdk。发现功能将搜索应用程序 composer.json 中要求的任何提供 psr/http-client-implementation 的包。为了简化解释,这里我故意省略了 psr/http-factory-implementation 虚拟包,但原理相同。
<?php
useHttp\Discovery\Psr17Factory;
useHttp\Discovery\Psr18ClientDiscovery;
usePsr\Http\Client\ClientInterface;
usePsr\Http\Message\RequestFactoryInterface;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Message\StreamFactoryInterface;
usePsr\Http\Message\StreamInterface;
namespaceFoo\SDK;
final readonly class Api
{
privateconst string BASE_URI = 'https://example.com';
publicfunction __construct(
private ?ClientInterface $client = null,
private ?RequestFactoryInterface $requestFactory = null,
private ?StreamFactoryInterface $streamFactory = null,
) {
$this->client = $client ?: Psr18ClientDiscovery::find();
$this->factory = new Psr17Factory(
requestFactory: $requestFactory,
streamFactory: $streamFactory,
);
}
publicfunction callApi(array $data): ResponseInterface
{
$body = $this->factory->createStream(json_encode($data));
$request = $this->factory->createRequest('POST', self::BASE_URI . '/api/bar')
->withHeader('Content-Type', 'application/json')
->withBody($body)
;
return$this->client->sendRequest($request);
}
}
这是一个 Foo 类由 SDK 提供的基本示例,利用 PSR-18 和 PSR-17 接口在其代码中。给 SDK 用户自由自带 HTTP 客户端:
Psr18ClientDiscovery::find()$client 参数传递给构造函数请注意,Psr17Factory 内部调用 Psr17FactoryDiscovery 方法来查找现有的具体工厂实现,如果传递 null 给构造函数。
<?php
namespace App;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
final readonly class FooRelatedService
{
public function callBar(array $data): void
{
// 在 Foo/SDK/Api 内部,Psr18ClientDiscovery 将搜索 PSR-18 的具体实现
$fooApi = new Foo\SDK\Api();
$response = $foo->callApi($data);
// 你的逻辑
}
public function callBarWithMyOwnHttpClient(array $data): void
{
// 独立实例化,但这里可以使用依赖注入。
$myOwnHttpClient = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(client: $myOwnHttpClient);
$response = $foo->callApi($data);
// 你的逻辑
}
public function callBarWithMyOwnHttpClientAndFactories(array $data): void
{
// Psr18Client 实现了 ClientInterface、RequestFactoryInterface、StreamFactoryInterface 等
$myOwnHttpClientAndFactories = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(
client: $myOwnHttpClientAndFactories,
requestFactory: $myOwnHttpClientAndFactories
streamFactory: $myOwnHttpClientAndFactories
);
$response = $foo->callApi($data);
// 你的逻辑
}
}
这是一个 SDK 用户代码的基本示例,他们让 SDK 找到合适的 HTTP 客户端(方法 callBar)或手动传递客户端(方法 callBarWithMyOwnHttpClient),甚至传递客户端和工厂(方法 callBarWithMyOwnHttpClientAndFactories)。当然,在传递这些对象之前,你可以根据需要配置它们(基础 URI、头部等)。
只要 SDK 依赖接口,SDK 用户就可以完全自由地创建自己的接口实现并传递给 SDK Api 对象。
当然,我们这里只是触及了这个主题的表面。在开发 SDK 过程中,可能会有更多问题,尤其是当你需要依赖 HTTP 客户端的特定功能时,而这些功能未被 PSR-18 覆盖。
你也可以问自己,如果 SDK 用户的应用程序依赖中没有兼容的 HTTP 客户端怎么办?是否应该默认提供一个?哪个?如何提供?作为 SDK 维护者,如何测试你的代码?