How to Make Your PHP SDK Client-Agnostic with PSR‑7, PSR‑17, and PSR‑18
Learn how to decouple a PHP SDK from a specific HTTP client by leveraging PSR‑7, PSR‑17, and PSR‑18 standards together with php‑http/discovery, enabling users to plug in Guzzle, Symfony HttpClient, or any compatible client while keeping the SDK lightweight and flexible.
Overview
This article shows how to break the hard dependency on a particular HTTP client in a PHP SDK. By using the PSR‑7, PSR‑17, and PSR‑18 interfaces together with php-http/discovery, SDK developers can let users choose any client they prefer, such as Guzzle or Symfony HttpClient.
PSR Standards
PSR‑7 defines the HTTP message interfaces (Request, Response, Stream, UploadedFile, etc.) that are used as the type hints for sending and receiving HTTP data. PSR‑18 provides a generic ClientInterface for sending a PSR‑7 request and receiving a PSR‑7 response, along with three exception interfaces for different failure types. PSR‑17 supplies factories for creating the PSR‑7 message objects.
Relevant PHP Packages
psr/http-client
Contains the PSR‑18 ClientInterface and related exception interfaces, allowing type‑safe code without tying the SDK to a concrete implementation.
php-http/discovery
A library (and Composer plugin from v1.17) that automatically discovers and installs concrete implementations of PSR‑17 and PSR‑18 at runtime.
It works together with two virtual packages:
psr/http-client-implementation & psr/http-factory-implementationThese virtual packages exist only in Packagist to signal that a real package provides an implementation of the corresponding PSR interfaces. In an SDK’s composer.json, requiring psr/http-client-implementation means the SDK needs any package that offers a PSR‑18 implementation.
Because psr/http-client already depends on psr/http-message, the SDK does not need to require the concrete message interfaces directly (e.g., RequestInterface, ResponseInterface).
The Composer provide field can be used to declare that a package supplies a specific virtual package. Example for symfony/http-client:
{
"provide": {
"psr/http-client-implementation": "1.0"
}
}Preparing Your Code
SDK Code Example
<?php
use Http\Discovery\Psr17Factory;
use Http\Discovery\Psr18ClientDiscovery;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
namespace Foo\SDK;
final readonly class Api
{
private const string BASE_URI = 'https://example.com';
public function __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,
);
}
public function 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);
}
}The SDK automatically discovers a PSR‑18 client via Psr18ClientDiscovery::find() if none is supplied. It also discovers concrete factories via Psr17FactoryDiscovery when the factory arguments are null.
User Code Example
<?php
namespace App;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
final readonly class FooRelatedService
{
public function callBar(array $data): void
{
// SDK will auto‑discover a PSR‑18 client
$fooApi = new Foo\SDK\Api();
$response = $fooApi->callApi($data);
// Your logic here
}
public function callBarWithMyOwnHttpClient(array $data): void
{
$myOwnHttpClient = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(client: $myOwnHttpClient);
$response = $fooApi->callApi($data);
// Your logic here
}
public function callBarWithMyOwnHttpClientAndFactories(array $data): void
{
$myOwn = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(
client: $myOwn,
requestFactory: $myOwn,
streamFactory: $myOwn,
);
$response = $fooApi->callApi($data);
// Your logic here
}
}These examples demonstrate three ways to provide an HTTP client to the SDK: automatic discovery, manual injection via the constructor, and injection of both client and factories.
Deep Dive
The article only scratches the surface. Real‑world SDK development may encounter situations where a required client feature is not covered by PSR‑18, or where no compatible client is present in the consuming application. In such cases, SDK maintainers must decide whether to ship a default client, how to expose additional configuration, and how to test the SDK against multiple client implementations.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
