Building ReST API clients in PHP the easy way

Building REST API in PHP the Easy WayLike everything else, there is an easy way and there is a hard way to build a ReST client in PHP. The hard way is to comb over an API spec PDF, start implementing each API operation one by one, and dealing with argument handling, request validation and response validation for each call.

ReST API documentation is getting better these days. As more people adopt Swagger 2.0 / OAI, it's becoming easier to generate user-facing documentation, API clients and even API services. There are many online tools that provide a GUI for building Swagger definitions such as APISpark by RestLet and SwaggerHub. You can even host your own or build locally using swagger-ui.

In this post, I will go over what it takes to build an API consumer client in PHP using Guzzle Services. There are some examples out there of how this is done for earlier versions of Guzzle, but I will focus on Guzzle 6+.

Composer

Like every modern PHP library, we will want to start out with a `composer.json` file. You can use the composer init command to create one for you.

composer init --stability dev --name 'foo-api/foo-php-rest-client' --type library --no-interaction
composer require guzzlehttp/guzzle-services:dev-guzzle6

The service definition

The service definition describes the API endpoint you are going to connect to. In previous version of guzzle-services, the service description was a JSON file with a DSL specification. The same description format applies in the 6.x version, but the factory method for loading the service description from a JSON file was removed so we can think of it as a more generalized data structure. The current documentation shows the definition being loaded as an array but it can still be stored in JSON or a YAML file and loaded into an associative array.

{
  "name": "Foo",
  "apiVersion": "2012-10-14",
  "baseUrl": "http://api.foo.com",
  "description": "Foo is an API that allows you to Baz Bar",
  "operations": {
    "GetFoo": {
      "httpMethod": "GET",
      "uri": "/foo",
      "summary": "Gets foo",
      "responseModel": "FooResponse"
    },
    "CreateFoo": {
      "httpMethod": "POST",
      "uri": "/foo",
      "summary": "Creates new foo",
      "responseModel": "FooResponse",
      "parameters": {
        "name": {
          "location": "json",
          "type": "string",
          "required": true
        },
        "description": {
          "location": "json",
          "type": "string",
          "required": true
        }
      }
    }
  },
  "models": {
    "BaseResponse": {
      "type": "object",
      "properties": {
        "success": {
          "type": "string",
          "required": true
        },
        "errors": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "The error code."
              },
              "message": {
                "type": "string",
                "description": "The detailed message from the server."
              }
            }
          }
        }
      },
      "additionalProperties": {
        "location": "json"
      }
    },
    "FooResponse": {
      "type": "object",
      "extends": "BaseResponse",
      "properties": {
        "Foo": {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "required": true
            },
            "name": {
              "type": "string",
              "required": true
            },
            "description": {
              "type": "string",
              "required": true
            }
          }
        }
      }
    }
  }
}

But, wait a minute

We were talking about Swagger and now we have to deal with a DSL. The service description was inspired by an early version of Swagger, but Swagger has changed. In some ways it's a good thing the service description hasn't changed. If you've already created a `service.json` file and you want to upgrade Guzzle, you don't have to re-write the `service.json` file.

If you already have a Swagger file, that's great. Swizzle is reportedly able to convert your Swagger spec to a service definition but I have yet to try it out.

The client

The Client class will be responsible for loading the service definition. It uses a factory method (`create`) that takes a config array. It will load the service definition from the `service.json` file as the service definition. This client also uses Guzzle default to add Basic Authentication to each request. 

<?php

namespace FooApi\RestClient;

use GuzzleHttp\Client;
use GuzzleHttp\Command\Guzzle\Description;
use GuzzleHttp\Command\Guzzle\GuzzleClient;

class FooClient extends GuzzleClient {

    public static function create($config = []) {
        // Load the service description file.
        $service_description = new Description(
            ['baseUrl' => $config['base_uri']] + (array) json_decode(file_get_contents(__DIR__ . '/../service.json'), TRUE)
        );

        // Creates the client and sets the default request headers.
        $client = new Client([
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
            'auth' =>  [$config['api_user'], $config['api_pass']],
        ]);

        return new static($client, $service_description, NULL, NULL, NULL, $config);
    }
}

Using the client

Using the client is quite simple. You just need to instantiate it using the `::create` method and then call operations directly. Operations are available as method calls (supplied via a magic method `__call`) so you can access them directly.

<?php
use FooApi\RestClient\FooClient;

$foo_client = FooClient::create([
  'base_uri' => 'https://example.com',
  'api_user' => 'Foo',
  'api_pass' => 'Pass',
]);
$result_object = $foo_client->GetFoo();
$result_array = $result_object->toArray();

There is another way to write this without using magic. You may wish to do this because your IDE doesn't recognize the method `::GetFoo` and shows it as highlighted text. In this case you can use the `::getCommand` method call.

<?php
use FooApi\RestClient\FooClient;

$foo_client = FooClient::create([
  'base_uri' => 'https://example.com',
  'api_user' => 'Foo',
  'api_pass' => 'Pass',
]);

$command = $foo_client->getCommand('GetFoo');
$result_object = $foo_client->execute($command);
$result_array = $result_object->toArray();

Writing tests

Good libraries have tests. By looking at a library's tests, you get a good idea of the quality of the library, as well as information on how it can be used. Lest's require phpunit to run our tests.

composer require phpunit/phpunit:~4.0 --dev

With API clients, you don't usually want to make real calls to an API client. That would be API integration testing. You should be able to assume the service is testing itself. You could use fake versions of your client for testing, but with Guzzle there is an easier way. You can just use the built in Guzzle test server. I won't go over this in any depth in this article but you can see the basic idea below. For more information on testing please reference the code in the foo-php-frest-client example repository.

<?php

public function setUp()
{
    parent::setUp();

    // Load and start the guzzle test server.
    require_once __DIR__ . '/../../vendor/guzzlehttp/guzzle/tests/Server.php';
    Server::start();
    register_shutdown_function(function(){Server::stop();});

    $this->client = FooClient::create([
        'base_uri' => Server::$url,
        'api_user' => $_SERVER['FOO_USER'],
        'api_pass' => $_SERVER['FOO_PASS'],
    ]);
}

public function testGetFoo() {
    $foo = [
        'id' => '1',
        'name' => 'Foo',
        'description' => 'The best ever Foo.',
    ];
    Server::enqueue([new Response(200, [], json_encode(['status' => 'OK','Foo' => $foo], TRUE))]);
    $response = $this->client->GetFoo();

    self::assertInstanceOf(ResultInterface::class, $response);
    self::assertEquals($foo, $response->toArray()['Foo']);
}

Finally

Writing an API client in PHP doesn't have to be painful. There are libraries for OAuth authentication which is a common way of authenticating API requests. Now that you know how to create an API client, share it with the rest of us so we can include it using Composer. Sharing is caring, as they say.