Breaking Free from Guzzle5 with PHP-HTTP and HTTPlug

Share this article

Key Takeaways

  • HTTPlug, a project by PHP-HTTP, allows for easy injection of any HTTP client into an SDK, providing a solution for applications that already have a preferred HTTP client in use and want to avoid using Guzzle.
  • The HTTPlug interface package and Guzzle 6 can be pulled in together using the composer require php-http/guzzle6-adapter command. This allows for the use of Guzzle 6 or any other adapter implementing HTTPlug’s HttpClient interface.
  • The PHP-HTTP project aims to have maximum support for all HTTP clients in PHP, including Guzzle 5 and 6, and Zend1 and 2. This allows for no conflicts with installed client versions and easy plugging in of the appropriate adapter.
  • The Diffbot SDK has been made PSR-7 compatible and receptive of other implementations of HTTP clients. It only needs an adapter that respects the HTTPlug interface for everything to work out of the box.
  • HTTPlug provides a new approach to abstracting HTTP client implementations in apps, offering extensibility for the cost of one additional layer of abstraction.

This article was peer reviewed by Márk Sági-Kazár and David Buchmann. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


In a previous series, we built a PHP client for Diffbot. The client works well and is in relatively widespread use – we even tested it on a live app to make sure it’s up to par – but it depends heavily on Guzzle 5.

There are two problems with this:

  1. Guzzle 6 is out, and supports PSR 7. While the author of Guzzle claims Guzzle 5 will be supported for the foreseeable future, it’s safer to be skeptical of its longevity. Besides, while PSR 7 might have its quirks, it’s good to follow PSRs if only for the compatibility with other projects
  2. Someone implementing our client in their app might already have a preferred HTTP client in use, and would like to use theirs rather than Guzzle. We should allow for easy injection of any HTTP client into our SDK.

Coincidentally, there is a new project allowing us to do just that: HTTPlug.

Travel power adapter

Note: you don’t need to be familiar with the internal logic of the Diffbot SDK to follow along. The process in this article is applicable to any package with a concrete HTTP Client implementation and is easy to follow.

PHP-HTTP and HTTPlug

PHP-HTTP is a Github organization for HTTP related tools in PHP. It provides HTTPlug, a collection of interfaces and exceptions to define a minimal HTTP client contract on top of PSR-7 request and response. Implementations of this contract ​provide​ the virtual package php-http/client-implementation.

This means someone who uses Guzzle 6 can composer require php-http/guzzle6-adapter to pull in the adapter, the HTTPlug interface package, and Guzzle 6 itself as a dependency of the adapter.

HTTPlug is the entry point for a reusable package. It is the client abstraction all clients (like the Guzzle6 Adapter) are based on. These clients then further make use of their underlying packages / dependencies – Guzzle 6 in this case.

So, bottom to top:

  • an HTTP Client exists (Guzzle 6)
  • a Guzzle 6 adapter is built with HTTPlug as the interface for it, wraps Guzzle 6
  • an app needing to be able to make HTTP calls needs a client, requires HTTPlug’s HttpClient interface rather than Guzzle 6 directly
  • the app can then use Guzzle 6, or any other adapter implementing HTTPlug’s HttpClient interface and wrapping another third party HTTP Client

The team’s plan is to eventually have maximum support for all the various HTTP clients in PHP land: Guzzle 6, Guzzle 5, Zend2, Zend1, etc. That way, a user of a framework or app will have no conflicts with installed client versions, and will simply plug the appropriate adapter into the mix.

Note that we use the terms adapter and client here almost interchangeably – the adapters based on HTTPlug are both. They are wrappers around existing clients, but used directly as clients themselves.

Our plan in this post is to replace the concrete Guzzle 5 dependency of Diffbot’s PHP client with the HTTPlug version.

Note: HTTPlug and related packages are alpha software, and as such are subject to change. Converting anything to use them is a risky endeavor.

Bootstrapping

As usual, it’s recommended we use Homestead Improved to bootstrap our environment. Once we’re ready, we can clone and test the current stable version of the SDK:

git clone https://github.com/swader/diffbot-php-client
cd diffbot-php-client 
git checkout tags/0.4.5 
composer install
phpunit

The last command assumes PHPUnit is globally installed on the development environment.

All tests should pass (except for a skipped one that’s bugged and unfixable due to some nonsense), so we’re ready to begin the conversion.

Getting Started

First, we’ll need to create a new branch on which to develop this upgrade.

git checkout -b feature-httplug

Then, we add two dependencies into our composer.json file:

	"require": {
        ...
        "php-http/client-implementation": "^1.0"
    },
    "require-dev": {
        ...
        "php-http/guzzle6-adapter": "~0.2@dev"
    },

What this does is tell the client that from now on, it depends on a virtual packagethis one. This means that in order to be used, the application using our Diffbot client (like this one) must select an implementation of this package (one of those listed at the link on Packagist). Of course, during development of the package, it would be impossible to test and see if everything’s working without an actual implementation, so we specify an additional require-dev dependency. In the specific case above, we use "php-http/guzzle6-adapter": "~0.2@dev". We chose that particular version simply because it’s the newest one and there’s no stable release.

Note: You may be wondering why we used the approach of adding values into composer.json rather than declaring dependencies interactively in the terminal like we usually do. This is because doing a composer require on a virtual package will throw an error – the package doesn’t really exist, it’s just its virtual name, a placeholder, so Composer will get confused not knowing what to install. There is an issue suggesting a change to this, but it’s not likely to happen soon.

Since the php-http packages are still under heavy development, we should add the following two values to our composer.json file:

"prefer-stable": true,
"minimum-stability": "dev"

This is to allow the installation of dev packages (non-stable), but will prefer stable versions if they exist. So rather than fetch, say, PHPUnit 5.2.x which is highly unstable, it will fetch 5.0.8 (most up to date at the time of writing), but it will also succeed if we ask it for packages that don’t have stable releases (like the guzzle6-adapter).

We also need to remove the dependency on Guzzle5, if we intend to install Guzzle6. The final require blocks look like this:

    "require": {
        "php" : ">=5.4.0",
        "php-http/client-implementation": "^1.0"
    },
    "require-dev": {
        "symfony/var-dumper": "~2",
        "phpunit/phpunit": "^5.0",
        "php-http/guzzle6-adapter": "~0.2@dev"
    },

The Plan

The way the SDK currently works is as follows: in the main Diffbot class, we optionally set an HTTPClient. This is currently bound to Guzzle’s implementation at version 5. If no custom client instance is set, the Diffbot class automatically uses a default client.

This client is then used by the Api Abstract’s call method to issue a get request to the given URL. Additionally, there is a custom call method in the Crawl API class and the Search API class.

The result of the call is saved as a $response, which is a Guzzle5 Response. That response is then additionally processed by the Entity Factory which checks its validity and builds entities from it, pushing them into Entity Iterator.

The plan is, thus, to:

  1. Replace Diffbot::setHttpClient with a method accepting an HTTPlug implementation
  2. Modify the API abstract’s, Crawl’s, and Search class’ call methods so that they can issue a GET request with any HTTP client implementation provided to them.
  3. Modify the Entity Factory and Entity Iterator so that they no longer depend on the Guzzle5 version of Response, but rather the PSR-7 counterpart.

The PHP-HTTP project has an additional package, Utils, which contains HttpMethodsClient. That class wraps a message factory and the HTTP client into one whole, making it easier to send requests with commonly used verbs like GET, POST, etc – thus translating into something similar to what we had so far: $client->get( ... ). What’s more, it also returns the PSR-7 ResponseInterface, which means the getBody method will be available to us – that’ll leave only the toJson method unimplemented, something we can easily do ourselves.

Additionally, the project has a Discovery component, which features some static classes for discovering installed factories and clients – this allows us to provide our end user with a zero-configuration experience in some cases (see docs).

With the battle plan laid out, we can get started with the refactoring.

Prerequisites

Let’s require the additional packages:

composer require "php-http/utils" "php-http/discovery"

Diffbot Class

The Diffbot class has this line at the top:

use GuzzleHttp\Client;

We can just change it to:

use Http\Client\Utils\HttpMethodsClient as Client;

The setHttpClient method should flare up in the IDE now, saying it’s missing some required parameters, namely the client to use, and the message factory with which to build Request instances.

The method should be refactored into:

/**
 * Sets the client to be used for querying the API endpoints
 *
 * @param Client $client
 * @see http://php-http.readthedocs.org/en/latest/utils/#httpmethodsclient
 * @return $this
 */
public function setHttpClient(Client $client = null)
{
    if ($client === null) {
		$client = new Client(
		   \Http\Discovery\HttpClientDiscovery::find(),
		   \Http\Discovery\MessageFactoryDiscovery::find()
		);
    }
    $this->client = $client;
    return $this;
}

Alternatively, the Discovery classes can be imported with use statements at the top of the class.

This change has now allowed the end user of the Diffbot SDK to either:

  • have their own client installed and let the Discovery components in tandem with HttpMethodsClient take care of things automatically, or
  • configure their own HttpMethodsClient instance by injecting a custom instance of a PSR 7 Client and Message Factory into a new instance of it, and inject that into the setHttpClient method for full flexibility

Most users will use this on autopilot.

Next up, the call methods.

Response instance flagged as erroneous

Because the HttpMethodsClient instance we implemented before has a get method, there are no changes needed in that regard. The $response instance, however, shows a mistmatch, and with good reason. The original $response expected by the EntityFactory is a Guzzle5 Response.

Due to the complaint being issued by EntityFactory, we don’t really need to edit the API Abstract – it’ll take care of things on its own. The Crawl class’ call counterpart is a bit different:

public function call()
{
    $response = $this->diffbot->getHttpClient()->get($this->buildUrl());

    $array = $response->json();

    if (isset($array['jobs'])) {
        $jobs = [];
        foreach ($array['jobs'] as $job) {
            $jobs[] = new JobCrawl($job);
        }

        return new EntityIterator($jobs, $response);
    } elseif (!isset($array['jobs']) && isset($array['response'])) {
        return $array['response'];
    } else {
        throw new DiffbotException('It appears something went wrong.');
    }
}

Two warnings here – the second line of the method which uses the json method of $response, and the EntityIterator instantiation which expects a Guzzle5 Response. The only line we can affect from here is the former, so let’s change it to:

$array = json_decode($response->getBody(), true);

A similar change needs to be done in the Search class’ call method, where the line:

$arr = $ei->getResponse()->json(['big_int_strings' => true]);

changes into:

$arr = json_decode((string)$ei->getResponse()->getBody(), true, 512, 1);

Entity Factory

The EntityFactory class has the following import at the top:

use GuzzleHttp\Message\ResponseInterface as Response;

We can change this to:

use Psr\Http\Message\ResponseInterface as Response;

The same needs to be done in the EntityFactory interface which the EntityFactory class implements.

The other change is similar to what we did above, in the Crawl class. We change:

$arr = $response->json(['big_int_strings' => true]);

to

$arr = json_decode($response->getBody(), true, 512, 1);

in both checkResponseFormat and createAppropriateIterator methods.

Entity Iterator

We change:

use GuzzleHttp\Message\ResponseInterface as Response;

to

use Psr\Http\Message\ResponseInterface as Response;

Tests

Mocking, the main way of testing HTTP requests and API calls, is different in Guzzle 6, so our tests need a slightly bigger overhaul.

As this tutorial is already a bit on the long side, please see the relevant feature branch if you’re interested in learning the differences in mocking between Guzzle 5 and Guzzle 6 and, specifically, between the two versions of the Diffbot SDK.

Finally, let’s run the tests:

phpunit

PHPUnit 5.0.8 by Sebastian Bergmann and contributors.

Runtime:       PHP 5.6.10-1+deb.sury.org~trusty+1 with Xdebug 2.3.2
Configuration: /home/vagrant/Code/diffbot-php-client/phpunit.xml.dist

...............................................................  63 / 347 ( 18%)
............................................................... 126 / 347 ( 36%)
............S.................................................. 189 / 347 ( 54%)
............................................................... 252 / 347 ( 72%)
............................................................... 315 / 347 ( 90%)
................................                                347 / 347 (100%)

Time: 55.78 seconds, Memory: 34.25Mb

Success! All passing (except the expected skipped test).

The Diffbot SDK is now not only PSR-7 compatible, but also receptive of other implementations of HTTP clients. All it needs is an adapter respecting the HTTPlug interface, and everything should work out of the box.

Conclusion

HTTPlug is a useful new approach to abstracting the HTTP client implementations in the apps we build. Whether we’re building HTTP clients ourselves or using them in other apps, PHP-HTTP provides a whole new world of extensibility for the reasonable price of one additional layer of abstraction.

If you’d like to help out by adding more adapter implementations, or just by trying the packages out and giving feedback, the team welcomes all contributions. Get in touch, or leave your feedback in the comments section below, and if you found this tutorial interesting, don’t forget to hit that like button!

Frequently Asked Questions (FAQs) about PHP HTTP and HTTPlug

What is the difference between Guzzle5 and PHP HTTP/HTTPlug?

Guzzle5 and PHP HTTP/HTTPlug are both HTTP clients used in PHP. Guzzle5 is a specific HTTP client, while PHP HTTP is an abstraction layer that allows you to use any HTTP client. HTTPlug is an extension of PHP HTTP that provides additional features. The main difference is that PHP HTTP and HTTPlug allow for more flexibility and interoperability, as they are not tied to a specific HTTP client.

How do I migrate from Guzzle5 to PHP HTTP/HTTPlug?

Migrating from Guzzle5 to PHP HTTP/HTTPlug involves replacing the Guzzle5 client with an adapter that implements the PHP HTTP interfaces. This can be done using the HTTPlug library, which provides adapters for various HTTP clients, including Guzzle5. Once the adapter is set up, you can use the PHP HTTP methods to send requests and handle responses.

What are the benefits of using PHP HTTP/HTTPlug over Guzzle5?

The main benefit of using PHP HTTP/HTTPlug over Guzzle5 is the increased flexibility and interoperability. With PHP HTTP/HTTPlug, you can switch between different HTTP clients without changing your code. This makes it easier to test your application with different clients and to switch clients if necessary. Additionally, HTTPlug provides a plugin system that allows you to add functionality to your HTTP client.

How do I install PHP HTTP/HTTPlug?

PHP HTTP/HTTPlug can be installed using Composer, a dependency management tool for PHP. You can install it by running the command composer require php-http/httplug. This will download the HTTPlug library and its dependencies.

How do I use PHP HTTP/HTTPlug to send a request?

To send a request with PHP HTTP/HTTPlug, you first need to create a request object. This can be done using the createRequest method of the MessageFactory interface. Once you have a request object, you can send it using the sendRequest method of the HttpClient interface.

How do I handle responses with PHP HTTP/HTTPlug?

Responses in PHP HTTP/HTTPlug are represented by the ResponseInterface interface. You can access the status code, headers, and body of the response using the getStatusCode, getHeaders, and getBody methods, respectively.

What is the purpose of the Discovery component in PHP HTTP/HTTPlug?

The Discovery component in PHP HTTP/HTTPlug is used to automatically find and use available HTTP adapters and message factories. This makes it easier to switch between different HTTP clients and to use the best available implementation.

How do I use the Discovery component in PHP HTTP/HTTPlug?

The Discovery component can be used by calling the static find method on the HttpClientDiscovery or MessageFactoryDiscovery classes. This will return an instance of the first available HTTP client or message factory.

What are the plugins in HTTPlug and how do I use them?

Plugins in HTTPlug are used to add functionality to the HTTP client. They can be used to add features like authentication, caching, and error handling. Plugins can be added to the client using the addPlugin method of the PluginClient class.

How do I handle errors in PHP HTTP/HTTPlug?

Errors in PHP HTTP/HTTPlug are represented by exceptions that implement the Http\Client\Exception interface. You can catch these exceptions and handle them as needed. Additionally, you can use the ErrorPlugin to automatically convert non-200 responses into exceptions.

Bruno SkvorcBruno Skvorc
View Author

Bruno is a blockchain developer and technical educator at the Web3 Foundation, the foundation that's building the next generation of the free people's internet. He runs two newsletters you should subscribe to if you're interested in Web3.0: Dot Leap covers ecosystem and tech development of Web3, and NFT Review covers the evolution of the non-fungible token (digital collectibles) ecosystem inside this emerging new web. His current passion project is RMRK.app, the most advanced NFT system in the world, which allows NFTs to own other NFTs, NFTs to react to emotion, NFTs to be governed democratically, and NFTs to be multiple things at once.

BrunoSDiffbotguzzleguzzlephphttp apihttp clientOOPHPPHPphp-httppsrpsr7
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week