Sep 03, 2014
Table of contents:
An API is an interface to an external system. By using an API we can interact with a third-party system regardless of how the internals of the third-party operate.
In order to transfer data from one system to another through an API we need a common format. Typically data is transferred between systems as either XML or JSON. In this package we will be using JSON, but the two formats are pretty much interchangeable.
Whilst it’s imperative that we can send and receive these common data formats, we don’t want to use this raw data internally.
Instead we need to transform the raw data into objects when it comes in, and serialise the data to JSON on the way out. This means we can work with the data using PHP objects, but all interactions with the outside world will use JSON.
In today’s tutorial we’ll be looking at smoothing over any idiosyncrasies the API might have so we can provide an easy to use interface for the developer.
As data comes in and goes out of our application, we need a way to transform it into something that is going to be useful.
When the data comes in we need a way of parsing the JSON and turning it into PHP model objects of the correct type.
As data goes out we need to serialise it so that the API will recognise and accept our request.
These processes in our SDK will be represented by Normalizer
and Serializer
classes. The Normalizer
class will sit just after we get the response back from the API and the Serializer
class will sit just before the request gets sent to the API.
Unfortunately, due to the nature of the API we’re working with and some idiosyncrasies of the data, we will need to provide some configuration options in our models to make this work smoothly.
One such area of concern we will need to tackle is how Organisations and People are represented in this API.
If you have a look at the documentation, you will see that both organisations and people are retrieved from the party
endpoint. This is because organisations and people are types of “party”.
This is fairly easy to represent in our SDK because we can simply extend the Party
model with a model for Organisation
and a model for Person
.
However when we use the Party
model to get all parties from the API, we need a way to interpret the response and hydrate each entity into the correct model instance.
In order to get around this problem, we can set an array of configuration options on the model instance that we can use to interpret how the SDK should react to the response.
As we continue building out this SDK we can leverage the same options array to configure certain other aspects of how the SDK should work with the data from the API, but for now we will concentrate on this one area.
The first thing we will do will be to create the Organisation
and Person
models. As I explained above, these two models are types of Party
so we can simply inherit from the Party
class.
The Organisation class looks like this:
<?php namespace PhilipBrown\CapsuleCRM;
class Organisation extends Party
{
/**
* The model's fillable attributes
*
* @var array
*/
protected $fillable = ["id", "name", "created_on", "updated_on"];
/**
* Create a new instance of the Organisation model
*
* @param PhilipBrown\CapsuleCRM\Connection $connection
* @param array $attributes;
* @return void
*/
public function __construct(Connection $connection, array $attributes = [])
{
parent::__construct($connection);
$this->fill($attributes);
}
}
And the Person
class looks like this:
<?php namespace PhilipBrown\CapsuleCRM;
class Person extends Party
{
/**
* The model's fillable attributes
*
* @var array
*/
protected $fillable = [
"id",
"first_name",
"last_name",
"created_on",
"updated_on",
];
/**
* Create a new instance of the Person model
*
* @param PhilipBrown\CapsuleCRM\Connection $connection
* @param array $attributes;
* @return void
*/
public function __construct(Connection $connection, array $attributes = [])
{
parent::__construct($connection);
$this->fill($attributes);
}
}
As you can see, we don’t need define that these two classes should use the Findable
trait because they are inheriting the ability from the Party
class.
In order to get this functionality to work, I want to be able to set an array of configuration options on each model instance. This array of options will hold any specific configuration details that the current model will require. I will also require a number of default options to be set that follow a specific convention.
For this specific bit of functionality, I want to be able to set two configuration options on a model for the root
and the collection_root
.
In the Party
class I can then set the root
to:
["person", "organisation"];
This will allow the SDK to know that the response for this model request should be interpreted as a subclass of the parent model.
If either of the two options are not set, the SDK should know to use the default setting that follows the convention. The convention should be to use the singular form of the model name for the root
and the plural form of the model name for the collection_root
.
Instead of adding this logic to the main Model
class, I will instead create it as a Serializable
trait that can be added to each model instance.
Before I get into the code, first I will write the tests to show how I intend the functionality to work.
/** @test */
public function should_get_serliazable_options()
{
$options = $this->model->serializableOptions();
$this->assertTrue(is_array($options));
$this->assertEquals('serializablemodelstubs', $options['collection_root']);
$this->assertTrue(is_array($options['root']));
}
So as you can see above, I want to provide a serializableOptions()
method that will return the options for the current model.
This method should return an array of options.
For the collection_root
I want to return a string that follows the convention I outlined above.
However for certain classes I want to be able to override this convention and return an array of subclasses for this model. You can see that by how I expect the root
value to be an array.
So far in this series I’ve been using a ModelStub
class in my tests. This was getting a bit over-complicated for the vast array of configuration options that will be required to test. So instead I’ve created model stubs for each individual test file.
The model stub for this test file looks like this:
class SerializableModelStub extends Model
{
use Serializable;
protected $serializableConfig = ["root" => ["person", "organisation"]];
public function __construct(Connection $connection, $attributes = [])
{
parent::__construct($connection);
$this->fill($attributes);
}
}
As you can see, I’ve set the $serializableConfig
array to include the root
key with an override array of options. I haven’t set the collection_root
as the code should be able to return the correct value from the convention.
You will also notice I’ve used the Serializble
trait. This will be where the logic for this functionality will sit.
My full test file looks like this:
use Mockery as m;
use PhilipBrown\CapsuleCRM\Model;
use PhilipBrown\CapsuleCRM\Connection;
use PhilipBrown\CapsuleCRM\Serializable;
class SerializableTest extends PHPUnit_Framework_TestCase
{
/** @var PhilipBrown\CapsuleCRM\Connection */
private $connection;
/** @var PhilipBrown\CapsuleCRM\Model */
private $model;
public function setUp()
{
$this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
$this->model = new SerializableModelStub($this->connection);
}
/** @test */
public function should_get_serliazable_options()
{
$options = $this->model->serializableOptions();
$this->assertTrue(is_array($options));
$this->assertTrue(is_array($options["root"]));
$this->assertEquals(
"serializablemodelstubs",
$options["collection_root"]
);
}
}
class SerializableModelStub extends Model
{
use Serializable;
protected $serializableConfig = ["root" => ["person", "organisation"]];
public function __construct(Connection $connection, $attributes = [])
{
parent::__construct($connection);
$this->fill($attributes);
}
}
Now that we have the tests in place we can implement the functionality to make them work.
The first thing I will do will be to add a property to the Model
class. This will ensure that this class property will be set as an array by default:
/**
* The model's serializable config
*
* @var array
*/
protected $serializableConfig = [];
Next I will create a new file called Serializable.php
to hold the trait.
The basic trait definition will look like this:
<?php namespace PhilipBrown\CapsuleCRM;
trait Serializable
{
/**
* Return the serializable options
*
* @return array
*/
public function serializableOptions()
{
}
}
Here I’ve defined the single public method that we are going to need.
In order for this to work we need to be able to set up the default options and then merge in the model specific options to override certain settings.
In order to do that we can create a private method that will set up the default options:
/**
* Set the serializable options array
*
* @return void
*/
private function setSerializableOptionsArray()
{
$this->serializableOptions = [
'root' => $this->base()->lowercase()->singular(),
'collection_root' => $this->base()->lowercase()->plural()
];
}
In this method I’m setting a class property to an array of options. If you have been following along with these posts you will recognise the code used to get the singular and plural names of the current model from this tutorial.
Finally the serializableOptions()
method can be defined like this:
/**
* Return the serializable options
*
* @return array
*/
public function serializableOptions()
{
$this->setSerializableOptionsArray();
return array_merge($this->serializableOptions, $this->serializableConfig);
}
First we set the defaults by calling the private setSerializableOptionsArray()
method.
Next we merge and return the defaults with the model specific overrides.
Now if you run the tests again you should see everything pass.
It’s usually fairly easy to pull data from an API because most API’s will send you either JSON or XML. However once you have the API payload, you need to do something with it to make it useful.
One of the complicated things with working with the CapsuleCRM API is the idea that both Organisations and People are both types of Party.
This complicates things for us because we need a way to dynamically determine if the response from the API is either a direct response, or a response that contains instances of these subclasses.
The majority of API’s won’t have this complication, but many do have conventions that should be addressed due to the nature of the application.
Hopefully this should highlight the fact that writing an API for your application is not something you can simply rush. There should be a lot of thought going in to how you write your API and how you expect others to consume it.