Overview
Break free from rigid dependencies in PHP SDKs. Learn how to use PSR-7, PSR-17, and PSR-18 standards along with php-http/discovery, allowing users to utilize their preferred HTTP clients, whether it’s Guzzle, Symfony HttpClient, or others. This is a must-read for PHP and Symfony developers.
This article discusses practical tips for SDK developers to allow users to bring their own HTTP clients instead of forcing them to use a client they did not choose. It also targets users who wish to use their favorite HTTP clients within supported SDKs.
You may be aware of the jungle of HTTP clients present in the PHP ecosystem:<span>Guzzle</span>, <span>Symfony HttpClient</span>, <span>Buzz</span>, <span>CakePHP</span>, <span>HTTP</span>, etc. As an SDK maintainer, supporting all these packages can be a nightmare. Fortunately, there are abstraction layers and technologies that can help us support the vast majority of clients. Let’s explore how to achieve this!
PSR
First, here are some PSRs that you can consider as popular technical recommendations for library developers and your own application code. These PSRs cover many topics such as coding style, autoloading, caching, containers, clocks, and the one we are interested in today: HTTP.
[PSR-7: HTTP Messages] This describes the interfaces for Request and Response (as well as some “subtypes” like Stream, UploadedFile, etc.), which is used by PSR-18 as the parameter and return type for the <span>sendRequest</span> method.
[PSR-18: HTTP Client] This PSR can be summarized as:
A generic interface for sending PSR-7 requests and returning PSR-7 responses. It provides the ClientInterface and three exception interfaces to represent different types of failures.
[PSR-17: HTTP Factory] This PSR provides factories for all HTTP message interfaces described in PSR-7. We will see how to leverage these factories below.
PHP Packages
psr/http-client
This package contains code related to PSR-18 (ClientInterface and three exception interfaces). This is exactly what we need to type our code and avoid dependencies on specific implementations.
php-http/discovery
This is both a code library and a Composer plugin (starting from v1.17). It is responsible for the automatic discovery and installation of concrete implementations of PSR-17 and PSR-18 interfaces.
It works alongside the next two packages.
psr/http-client-implementation & psr/http-factory-implementation
These two packages are a bit special because they are virtual packages. They only exist in the Packagist registry and are used by “real” packages to indicate that they provide concrete implementations of the PSR-17 and/or PSR-18 interfaces.
A quick note on the concept of virtual packages: think of them as package-level PHP interfaces. Here, in the SDK’s composer.json, declaring the virtual package psr/http-client-implementation indicates that the SDK requires any package that provides a concrete implementation of PSR-18.
Since psr/http-client directly depends on psr/http-message, we do not need to depend on it to reference message interfaces like <span>RequestInterface</span>, <span>ResponseInterface</span>, etc.
One last useful piece of knowledge is the <span>provide</span> property in Composer. Here is a specific usage:
- For symfony/http-client maintainers, you can add this property in your composer.json file to indicate to other developers that the symfony/http-client package provides a concrete PSR-18 implementation.
"provide": {
"psr/http-client-implementation": "1.0",
},
- For SDK developers, you can require this virtual package psr/http-client-implementation along with php-http/discovery, which acts as a plugin that will handle understanding that the SDK needs a PSR-18 implementation. It will then look for one in the installed application’s composer.json (reading the
<span>provide</span>property of each dependency). - A picture is worth a thousand words, here is a diagram:
img
A diagram showing how HTTP clients collaborate with PSR
Thanks to php-http/http-discovery required by foo/sdk, developers of both applications only need to require their preferred HTTP client and foo/sdk. The discovery feature will search for any package in the application’s <span>composer.json</span> that provides psr/http-client-implementation. To simplify the explanation, I intentionally omitted the psr/http-factory-implementation virtual package, but the principle is the same.
Prepare Your Code
SDK Code Example
<?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);
}
}
This is a basic example of a Foo class provided by the SDK, utilizing PSR-18 and PSR-17 interfaces in its code. It gives SDK users the freedom to bring their own HTTP clients:
- Automatically, via
<span>Psr18ClientDiscovery::find()</span> - Manually, by passing the
<span>$client</span>parameter to the constructor
Note that the <span>Psr17Factory</span> internally calls the <span>Psr17FactoryDiscovery</span> method to find existing concrete factory implementations if null is passed to the constructor.
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
{
// Inside Foo/SDK/Api, Psr18ClientDiscovery will search for concrete implementations of PSR-18
$fooApi = new Foo\SDK\Api();
$response = $foo->callApi($data);
// Your logic
}
public function callBarWithMyOwnHttpClient(array $data): void
{
// Independently instantiated, but can use dependency injection here.
$myOwnHttpClient = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(client: $myOwnHttpClient);
$response = $foo->callApi($data);
// Your logic
}
public function callBarWithMyOwnHttpClientAndFactories(array $data): void
{
// Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, etc.
$myOwnHttpClientAndFactories = new Psr18Client(HttpClient::create());
$fooApi = new Foo\SDK\Api(
client: $myOwnHttpClientAndFactories,
requestFactory: $myOwnHttpClientAndFactories,
streamFactory: $myOwnHttpClientAndFactories
);
$response = $foo->callApi($data);
// Your logic
}
}
This is a basic example of user code for the SDK, allowing the SDK to find the appropriate HTTP client (method <span>callBar</span>) or manually passing a client (method <span>callBarWithMyOwnHttpClient</span>), or even passing clients and factories (method <span>callBarWithMyOwnHttpClientAndFactories</span>). Of course, you can configure these objects as needed (base URI, headers, etc.) before passing them.
As long as the SDK depends on interfaces, SDK users can freely create their own interface implementations and pass them to the SDK <span>Api</span> object.
Diving Deeper
Of course, we have only scratched the surface of this topic. During the SDK development process, there may be more questions, especially when you need to rely on specific features of the HTTP client that are not covered by PSR-18.
You may also ask yourself, what if there is no compatible HTTP client in the SDK user’s application dependencies? Should a default one be provided? Which one? How to provide it? As an SDK maintainer, how do you test your code?