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.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
How to Make Your PHP SDK Client-Agnostic with PSR‑7, PSR‑17, and PSR‑18

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-implementation

These 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

SDKPHPComposerHTTP clientPSR-17PSR-18PSR-7
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.