Bring Your Own HTTP client

· Mathieu Santostefano · 5 minutes to read
3 dog heads

Break free from rigid dependencies in your PHP SDKs. Learn how to use PSR-7, PSR-17, and PSR-18 standards along with php-http/discovery to allow users to bring their favorite HTTP client, whether it's Guzzle, Symfony HttpClient, or another. A must-read for PHP and Symfony developers.

This article is about practical techniques for SDK developers to allow users to bring their own HTTP client instead of forcing utilization of one they did not choose. It is also aimed at users who would like to use their favorite HTTP client within an SDK that already supports it.

You probably know the jungle of HTTP Clients that exist in the PHP ecosystem: Guzzle, Symfony HttpClient, Buzz, CakePHP, HTTP, and so on. As an SDK maintainer, it’s kind of a nightmare to provide support for all those packages. Fortunately, there are abstraction layers and techniques to help us embrace the large majority of them. Let’s explore how to achieve this!

Genesis

A few months ago, I attended AFUP Day 2025, a one-day French conference, and especially to a talk from my friend Nicolas Grekas about HTTP Client in PHP. The talk was not exactly the way I was thinking of. It placed the audience in the shoes of a SDK developer, facing the dilemma of providing an HTTP client in your source code dependencies:

  • Which HTTP client?

  • What if an SDK user already has an HTTP client installed in their dependencies?

  • How can an SDK user utilize their own HTTP client instead of the one I provide?

Most of these questions have been answered by Nicolas, in an elegant way. Let’s dive into this fascinating topic!

Here is a record of this talk during API Platform Con 2024: API Platform Conference 2024 - Nicolas Grekas - Consuming HTTP APIs in PHP the Right Way!

Equip yourself with the right tools

First, let me introduce you to some PSRs (PHP Standard Recommendations) and (not so magical) PHP packages capable of helping us on our quest.

PSR

First, here are some PSRs, if you aren’t familiar with this, see them as popular technical recommendations both for library developers and your own application code. These PSRs cover many topics such as coding style, autoloading, cache, container, clock and what we are interested in today: HTTP.

[PSR-7: HTTP Message]

This one describes interfaces for Request and Response (and some “sub-types” such as Stream, UploadedFile, etc), and it’s used by PSR-18 as argument type and return type for the sendRequest method.

[PSR-18: HTTP Client]

This PSR can be resumed as:

> A common interface for sending PSR-7 requests and returning PSR-7 responses.

It provides a ClientInterface and 3 Exception interfaces to represent different kinds of failures.

[PSR-17: HTTP Factories]

This PSR provides factories for all HTTP message interfaces described in PSR-7. We will see below how to take advantage of these factories.

PHP Packages

psr/http-client

This package contains the code related to PSR-18 (ClientInterface and 3 exception interfaces). This is exactly what we need to type our code and avoid relying on a concrete implementation.

php-http/discovery

This one is both a code library and a Composer plugin (since v1.17). It’s responsible for auto-discovery and auto-installation of packages providing concrete implementations of PSR-17 and PSR-18 interfaces.

It works alongside the next 2 packages.

psr/http-client-implementation & psr/http-factory-implementation

These 2 packages are a little bit special, as they are virtual packages. They only exist in packagist registry, and are used by “real” packages to indicate that they provide compliant implementations for PSR-17 and/or PSR-18 interfaces.

A quick note about the concept of virtual packages: see it like PHP interfaces but at the package level. Here, psr/http-client-implementation virtual package as requirement in a SDK composer.json indicates that the SDK needs any package providing a concrete implementation of PSR-18.

As psr/http-client directly depends on psr/http-message, we don’t need to depend on it to refer to message interfaces such as RequestInterface, ResponseInterface, etc.

One last useful thing to know is the provide property of Composer schema. Here is a concrete usage of this property:

  • For symfony/http-client maintainers, this property can be added into 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 an SDK developer, you can require this virtual package psr/http-client-implementation alongside php-http/discovery` which, as a plugin, will take care of understanding that the SDK requires a PSR-18 implementation. It will then look for one (reading the provide property of each dependency) in the composer.json of the application in which it is installed.

  • A picture is worth a thousand words, here is a picture:

Scheme showing how HTTP client works with PSRsThanks to php-http/http-discovery required by foo/sdk, the developers of both applications can only require their favorite HTTP client and foo/sdk. The discovery feature will seek to any package required by the composer.json of the application that provides psr/http-client-implementation.To simplify the explanation here, I deliberately omit the psr/http-factory-implementation virtual package, but the principle is the same.

Prepare 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);
    }

}

Here is a basic example of a Foo class provided by an SDK, taking advantage of PSR-18 and PSR-17 interfaces in its own code. Giving the freedom to SDK users to bring their own HTTP client: 

  • automatically, thanks to Psr18ClientDiscovery::find()

  • manually by passing a $client argument to the constructor

Note here that the Psr17Factory internally calls the Psr17FactoryDiscovery methods to find out existing concrete implementations of Factories needed 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 seek for a concrete implementation of PSR-18
        $fooApi = new Foo\SDK\Api();
        $response = $foo->callApi($data);

        // your logic
    }

    public function callBarWithMyOwnHttpClient(array $data): void
    {
        // Standalone instanciation, but dependency injection could be used 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 and others
        $myOwnHttpClientAndFactories = new Psr18Client(HttpClient::create());

        $fooApi = new Foo\SDK\Api(
            client: $myOwnHttpClientAndFactories,
            requestFactory: $myOwnHttpClientAndFactories
            streamFactory: $myOwnHttpClientAndFactories
        );
        $response = $foo->callApi($data);

        // your logic
    }
}

Also a basic example of SDK user code, where they let the SDK find the proper HTTP client (method callBar) or they pass manually a client (method callBarWithMyOwnHttpClient), even passing the client and the Factories (method callBarWithMyOwnHttpClientAndFactories). Of course, before passing those objects you can configure them for your needs (base uri, headers, etc.).

As long as the SDK relies on interfaces, the SDK user is totally free to create their own interface implementations and pass them to the SDK Api object.

Going further

Of course, we've only skimmed the surface of the topic here. A lot more questions could come during the development of an SDK, especially if there comes a moment when you need to rely on a specific feature of a HTTP client, not covered by PSR-18. 

You also can ask yourself what happens if a SDK user doesn’t have a compatible HTTP client in their application dependencies? Should you provide one by default? Which one? How to provide it? How to test your code as an SDK maintainer? 

If you liked this article and you want to go deeper on the topic, let us know! Maybe a second part will come to complete this first chapter!

Resources

Want to implement this strategy in your own SDK but not sure where to start?

SensioLabs, the creator of Symfony, offers specialized consulting and training to help your team master best practices for modern PHP development and HTTP client integration.

This might also interest you

Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025: Our Recap and the Highlights

After an iconic first edition in 2019, SymfonyCon made its big comeback to Amsterdam. From the start, you could feel the energy of a highly anticipated conference: more than 1,200 attendees, 39 nationalities, the biggest Symfony community reunion of the year, great discoveries... and a fun atmosphere. This year was extra special because it was the 20th anniversary of Symfony. SensioLabs was there: we'll tell you all about our experience there!

Read more
The SensioLabs team celebrating the 20th anniversary of Symfony with balloons
Jules Daunay

The Story Continues: SensioLabs Celebrates Symfony's 20th Anniversary

Time flies, especially when you're busy shaping the future of development! The SensioLabs team has just reached a milestone with the anniversary of the Symfony framework. We marked the occasion at the office, but the party isn't over yet. The date is already set for an XXL celebration at SymfonyCon Amsterdam 2025, from November 27 to 28.

Read more
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's New URI Extension: A Game-Changer for URL Parsing

PHP 8.5 introduces a powerful new URI extension that modernizes URL handling. With support for both RFC 3986 and WHATWG standards, the new Uri class provides immutable objects, fluent interfaces, and proper validation - addressing all the limitations of the legacy parse_url() function. This guide shows practical before/after examples and explains when to use each standard.

Read more
Open in new tab
Silas Joisten

The Tab Trap: Why Forcing New Tabs Is Bad UX

We’ve all done it — added target="_blank" to a link to “help users” stay on our site. But what feels like a harmless convenience often creates confusion, breaks accessibility, and introduces hidden security risks.

Read more
Blue sign on a building with several Now What? letters
Thibaut Chieux

How To Prioritize Messages When Building Asynchronous Applications With Symfony Messenger

Asynchronous processing offers benefits like decoupled processes and faster response times, but managing message priorities can become a challenge. When dealing with tasks ranging from password resets to complex exports, ensuring timely delivery of critical messages is essential. This article explores common asynchronous processing issues and provides solutions using Symfony Messenger, allowing you to optimize your application without extensive refactoring.

Read more
SensioLabs University Courses announcing the new level 3 Master training course now available
Jules Daunay

Master Symfony: Unlock Expert Skills with Our Training

Take your Symfony proficiency from good to great with the new Level 3 training course at SensioLabs! Master complex topics, optimize performance, and become a Symfony expert.

Read more
PHP 8.5
Oskar Stark

What's New in PHP 8.5: A Comprehensive Overview

PHP 8.5 will be released in November 2025 and brings several useful new features and improvements. This version focuses on developer experience enhancements, new utility functions, and better debugging capabilities.

Read more
Two images: on the left many cars stuck in a traffic jam with the sign "All directions" above, on the right a blue car moving forward alone on the highway with the sign "Service Subscriber" and a Symfony logo above
Steven Renaux

Symfony Lazy Services with Style: Boost DX using Service Subscribers

Boost your Symfony app's performance and developer experience! Learn how to use Service Subscribers and traits for lazy service loading to reduce eager instantiation, simplify dependencies, and create modular, maintainable code.

Read more
Image