How we consume APIs at SIMgroep

(This post was originally written by Richard van den Brand for the SIMgroep Developers Blog, when it wasn’t yet hosted on Medium.)

At SIMgroep one our of specialities is building nice frontends for a variety of mid- or backoffice products used at Dutch municipalities. This means that we consume a hell-of-a-lot of APIs. Most of them are — unfortunately — still
SOAP, but we see more and more RESTful APIs. This post will be about the latter: implementing and consuming RESTful APIs in PHP. Like a boss.

The Guzzle problem

When Guzzle was introduced back in 2011 it quickly became popular. Finally we had some nice abstraction around sending and receiving HTTP requests. Until then, most of us used curl_exec and friends for that. Especially when Composer was released it became very easy to make use of Guzzle. More and more repositories popped up built on Guzzle (I even wrote a couple of 
'em).

Development continued and during the years a lot changed: a few major versions were released (current stable is version 6) and they changed GitHub repositories. One of the side effects of this is that a lot of packages out there are locked to an outdated Guzzle version. If you’re using two different packages, which both require require a different major version of Guzzle, then problems start to arise:

Your requirements could not be resolved to an installable set of packages.
Problem 1
- Can only install one of: guzzlehttp/guzzle[6.2.2, 5.3.x-dev].
- Can only install one of: guzzlehttp/guzzle[5.3.x-dev, 6.2.2].
- Can only install one of: guzzlehttp/guzzle[5.3.x-dev, 6.2.2].
- ricbra/php-discogs-api 1.3.0 requires guzzlehttp/guzzle ~5.0 -> satisfiable by guzzlehttp/guzzle[5.3.x-dev].
- Installation request for ricbra/php-discogs-api ^1.3 -> satisfiable by ricbra/php-discogs-api[1.3.0].
- Installation request for guzzlehttp/guzzle (locked at 6.2.2, required as ~6.0@dev) -> satisfiable by guzzlehttp/guzzle[6.2.2].

Meet HTTPlug

What started as Ivory Http Adapter has evolved into HTTPlug with its own dedicated organization and website. From the website:

HTTPlug allows you to write reusable libraries and applications that need an HTTP client without binding to a specific implementation. When all packages used in an application only specify HTTPlug, the application developers can choose the client that best fits their project and use the same client with all packages.

So, by using HTTPlug in both clients and consuming projects we are able to finally solve these issues. This is hard for legacy projects, but for all new stuff we make use of HTTPlug. In the next paragraphs I’ll guide you on how to accomplish this.

Implementing a client

Whenever the need arises for a new client, we start by creating a composer.json file like this:

{
"require": {
"psr/http-message": "^1.0",
"php-http/client-implementation": "^1.0",
"php-http/httplug": "^1.0",
"php-http/message-factory": "^1.0",
"php-http/discovery": "^1.0",
"php-http/logger-plugin": "^1.0"
},
"require-dev": {
"php-http/mock-client": "^0.3",
"php-http/message": "^1.0",
"guzzlehttp/psr7": "^1.0",
"phpunit/phpunit": "^5.6",
"php-http/guzzle6-adapter": "^1.1"
}
}

Of course there are some more packages, but I left them out for the sake of simplicity. What’s important to note here is that we’re relying on php-http/client-implementation which is a virtual package. Basically that means that it's up to the consuming party to decide which concrete implementation they will use (more on that later).

The php-http/mock-client is a concrete implementation of php-http/client-implementation which makes it easier to write tests so we use that for development. The other packages are some helper packages, I suggest you take a look at the README of each package if you want to learn more.

Now let’s implement a simplified client:

<?php
namespace Simgroep\AcmeClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
class AcmeClient
{
/**
* @var HttpClient
*/
private $httpClient;
/**
* @var string
*/
private $endpoint;
/**
* @param HttpClient $httpClient
* @param string $endpoint
*/
public function __construct(HttpClient $httpClient, $endpoint)
{
$this->httpClient = $httpClient;
$this->endpoint = $endpoint;
}
public function awesomeApiCall($someParam)
{
$factory = MessageFactoryDiscovery::find();
$params = [
'some_param' => $someParam,
];
$request = $factory->createRequest(
'GET',
$this->endpoint,
[
'Content-Type' => 'application/x-www-form-urlencoded'
],
http_build_query($params)
);
$response = $this->httpClient->sendRequest($request)->getBody()->getContents();
return json_decode($response, true);
}
}

By using the MessageFactoryDiscovery we don't rely on a concrete implementation, but let the consuming party decide which implementation to use. We also like to test this client:

<?php
class AcmeClientTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function it_calls_endpoint_with_correct_params()
{
$mockClient = new MockClient();
$mockClient->addResponse(
MessageFactoryDiscovery::find()->createResponse(
200,
null,
[],
file_get_contents(__DIR__ . '/../resources/awesome')
)
);
$client = new AcmeClient($mockClient, 'http://acme.com/awesome');
$client->awesomeApiCall('123');
$request = $mockClient->getRequests()[0];
$this->assertSame(
'http://acme.com/awesome?some_param=123',
(string) $request->getUri()
);
$this->assertSame(
'GET',
$request->getMethod()
);
}
}

Alright, we’re all set now. We push our new client and make sure it’s registered in Satis or Packagist. Let’s move on and see how to consume it in one of our projects.

Using a client

At SIMgroep we like Symfony and thus we’re also using Symfony in this example. Let’s create a fresh Symfony installation using the symfony cli tool:

$ symfony new test
Downloading Symfony...
5.5 MiB/5.5 MiB ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  100%
Preparing project...
✔  Symfony 3.2.7 was successfully installed. Now you can:
* Change your current directory to /Users/richard/projects/test
* Configure your application in app/config/parameters.yml file.
* Run your application:
1. Execute the php bin/console server:start command.
2. Browse to the http://localhost:8000 URL.
* Read the documentation at http://symfony.com/doc

Next step would be to require our new client:

$  composer require simgroep/acme-client
Using version ^0.0.1 for simgroep/acme-client
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Installation request for simgroep/acme-client ^0.0.1 -> satisfiable by simgroep/acme-client[0.0.1].
- simgroep/acme-client 0.0.1 requires php-http/client-implementation ^1.0 -> no matching package found.
Potential causes:
- A typo in the package name
- The package is not available in a stable-enough version according to your minimum-stability setting
see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details.
Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems.
Installation failed, reverting ./composer.json to its original content.

Unfortunately this doesn’t work. This is because of the virtual packages we talked about earlier. Let’s add a Guzzle 6 adapter and also require php-http/message and guzzlehttp/psr7 so we have a concrete message implementation:

$ composer require simgroep/acme-client php-http/guzzle6-adapter guzzlehttp/psr7 php-http/message

After installation is done, all requirements are met and we can start using the new client…. or not? It turns out we also need to register a concrete client implementation in Symfony. This can be done manually, but I like to use the php-http/httplug-bundlefor this. Start by requiring it:

$ composer require php-http/httplug-bundle

Enable the bundle:

public function registerBundles()
{
$bundles = array(
// ...
new Http\HttplugBundle\HttplugBundle(),
);
}

Configure the bundle to use Guzzle 6:

httplug:
plugins:
logger: ~
clients:
default:
factory: 'httplug.factory.guzzle6'
plugins: ['httplug.plugin.logger']
config:
timeout: 2
# this is added so we can view the contents of the request bodies; be careful with increasing this value (memory)
toolbar:
captured_body_length: 20000

The bundle will register a service with id httplug.client.default which we can use to construct our client:

<services>
<service id="simgroep.acme_client" class="Simgroep\Acme\Client">
<argument type="service" id="httplug.client.default" />
</service>
</services>

To perform a call in your controller:

<?php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class ApiController extends Controller
{
public function awesomeAction()
{
$client = $this->get('simgroep.acme_client');
$response = $client->awesomeApiCall('123');
// do awesome stuff with $response
}
}

We’re using the FrameWorkBundle Controller here for the sake of simplicity, don’t use it in your projects ;).

If you open your Symfony application in dev mode, notice the new tab in the debug toolbar which lets you inspect all requests and responses sent via the new client. Awesome!

Also there are a bunch of plugins available with outstanding documentation. They will cover your back in most use cases!

Conclusion

As you should have seen by now, HTTPlug is an awesome initiave which makes it even more easy for us developers to consume RESTful APIs. For HTTPlug to succeed it’s important that developers who implement new clients stop depending on concrete implementations and only depend on HTTPlug. By doing so, they enable consuming parties to choose whatever client they want. As a bonus you get the fully featured Symfony bundle, tons of plugins and PSR-7.