Breaking Free from Guzzle5 with PHP-HTTP and HTTPlug

Bruno Skvorc
Share

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!