Special Anniversary Black Friday: Get 30% off all training and 10% off all services Get a Quote


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

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
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

HTTP Verbs: Your Ultimate Guide

HTTP Verbs Explained: Learn the basics of GET, POST, PUT, DELETE, and more. This article explains how they work, their applications, and their security implications.

Read more
Poster of Guillaume Loulier presentation
Salsabile El-Khatouri

A Symfony Training at SensioLabs: Behind The Scenes

What does Symfony training at SensioLabs look like? Find out in this interview with Guillaume Loulier, a passionate developer and trainer, who tells us all about the official Symfony training courses.

Read more
Domain Driven Design practical approach
Silas Joisten

Applying Domain-Driven Design in PHP and Symfony: A Hands-On Guide

Learn how to apply Domain-Driven Design (DDD) principles in Symfony with practical examples. Discover the power of value objects, repositories, and bounded contexts.

Read more
Photo speaker meetup AI Symfony
Jules Daunay

Symfony and AI: the video is now available

What about Symfony and Artificial Intelligence (AI)? This was the theme of the exclusive event organized by SensioLabs in partnership with Codéin on October 3rd. With the added bonus of feedback from a development project combining Symfony and AI. If you missed the event, check out the video now available for free on our Youtube channel.

Read more
2025 a year of celebrations for PHP with windows about API Platform, PHP, AFUP and Symfony
Jules Daunay

2025: a year of anniversaries for PHP, AFUP, Symfony and API Platform

2025 is going to be a big year for anniversaries. We will be celebrating the 20th anniversary of Symfony, the 30th anniversary of PHP, the 25th anniversary of AFUP and the 10th anniversary of API Platform. For SensioLabs, this is a major milestone that proves the longevity of the technologies in our ecosystem. We are proud to celebrate these anniversaries with the community all year long.

Read more
SymfonyDay Chicago 2025
Simon André

SymfonyDay Chicago 2025: A Celebration of Community

On March 17th, the Symfony community met in Chicago for SymfonyDay Chicago 2025. The event, held on St. Patrick's Day, was both a celebration of Symfony and a moment to support Ryan Weaver in his fight against cancer. It was more than just a conference — it was a gathering around a valued member of the community.

Read more
Blue ElePHPant on a computer
Imen Ezzine

Optimize Your PHP Code: 8 Functions You Need for Efficient Table Handling

If you want to become a good PHP developer, you must learn to work with arrays. Arrays are used a lot in PHP: temporarily store, organize, and process data before it's saved in a database. Knowing how to work with them efficiently will help you manage and process data more effectively.

Read more
type-safety-uuid
Oskar Stark

Type-Safe Identifiers with Symfony and Doctrine: Using Dedicated ID Classes

Learn how to enhance type safety in Symfony and Doctrine by using dedicated ID classes like BookId and UserId instead of raw UUIDs. This approach prevents identifier mix-ups, improves code clarity, and ensures better integration with Symfony Messenger and repository methods. Explore practical examples and best practices for implementing type-safe identifiers in your Symfony applications.

Read more
Image