Sep 17, 2014
Table of contents:
Last week we began looking at encapsulating the logic around normalising API JSON responses and turning them into model objects.
Whenever we make a request to the API, we will receive a raw JSON response. This JSON response will need to be “normalised” and converted into instances of the model objects that we will create as part of this SDK package.
Responses are either a single entity, such as when you retrieve the details of a single user, or a collection of entities, such as when you retrieve the details of all users.
We also have the added complication of parent and child entity relationships. For example, both Organisation and Person entities are child entities of the Party entity.
This normalisation process will sit just after we get the raw JSON response from the API. We need to provide a unified interface, whilst dealing with the logic around normalising all the different types of responses inside the Normalizer
class.
In today’s tutorial we continue to look at how we can implement this functionality. If you missed last week’s post, you should go read that first.
If you have been following along with this series you will remember from last week that we face the predicament of certain models having subclasses. For example, both Organisation
and Person
are subclasses of Party
.
This means if we are using the Party
model, we need a way of recognising that the response should be split into child classes. In order to do that we defined the root
of the $serializableConfig
as an array.
To check for subclasses we can write a private method like this:
/**
* Check to see if the entity has subclasses
*
* @return bool
*/
private function hasSubclasses()
{
return is_array($this->root());
}
This method checks to see if the return value of another private method is an array.
The root()
method looks like this:
/**
* Get the root of the entity
*
* @return array|string
*/
private function root()
{
if ($this->root) {
return $this->root;
}
if (isset($this->options['root'])) {
return $this->root = $this->options['root'];
}
return $this->root = $this->model->serializableOptions()['root'];
}
First we check to see if the $root
class property has been set and return it if it has.
Next we check to see if the root
has been passed in through the $this->options
array and return it.
If both of these if
statements fail we can fall back to the default from the serializableOptions()
method of the model. We can set the class property and then return it from the method.
We will also need to set $root
as a property of the class:
/**
* The root of the entity
*
* @var array|string
*/
protected $root;
Finally we can update the model()
and collection()
methods to run this check:
/**
* Normalize a single model
*
* @param array $attributes
* @return PhilipBrown\CapsuleCRM\Model
*/
public function model(array $attributes)
{
if ($this->hasSubclasses()) {
}
}
/**
* Normalize a collection of models
*
* @param array $attributes
* @return Illuminate\Support\Collection
*/
public function collection(array $attributes)
{
if ($this->hasSubclasses()) {
}
}
If we take a look at the API documentation we can see that a typical single subclass entity response looks like this:
{
"person": {
"id": "100",
"firstName": "Eric",
"lastName": "Schmidt",
"createdOn": "2011-09-14T15:22:01Z",
"updatedOn": "2011-12-14T10:45:46Z"
}
}
In the model()
method, if the current model instance has subclasses, we can pass the payload to a normalizeSubclass()
method:
/**
* Normalize a single model
*
* @param array $attributes
* @return PhilipBrown\CapsuleCRM\Model
*/
public function model(array $attributes)
{
if ($this->hasSubclasses()) {
return $this->normalizeSubclass($attributes);
}
}
The normalizeSubclass()
will look like this:
/**
* Normalize a subclass
*
* @param array $attributes
* @return PhilipBrown\CapsuleCRM\Model
*/
private function normalizeSubclass(array $attributes)
{
reset($attributes);
$key = key($attributes);
return $this->createNewModelInstance($key, $attributes[$key]);
}
First we use PHP’s reset() function to rewind the array’s internal pointer to the first element.
Next we use PHP’s key() function to return the key of the first element.
This will enable us to get the person
key from the JSON response so we know what type of model the current entity response is.
Finally we pass the $key
and the $attributes[$key]
to a createNewModelInstance()
method that will take of actually instantiating the new model instance.
To dynamically create a new instance of a model from the JSON response we can use the following method:
/**
* Create a new model
*
* @param string $name
* @param array $attributes
* @return PhilipBrown\CapsuleCRM\Model
*/
private function createNewModelInstance($name, array $attributes)
{
$class = ucfirst($name);
$class = "PhilipBrown\CapsuleCRM\\$class";
$attributes = Helper::toSnakeCase($attributes);
return new $class($this->model->connection(), $attributes);
}
In the method above I’m taking the $name
of the model and turning it into the full namespace of the class.
I’m also transforming all of the attributes to snake case. This is an optional step as I prefer to have model attributes as snake case, rather than camel case. You can see the code for the Helper
class here.
Querying for a collection of subclass entities is a little bit more difficult.
As with the single entity example, first we check for sub classes. If the response does have subclasses we can pass the response to a normalizeSubclassCollection()
method:
/**
* Normalize a collection of models
*
* @param array $attributes
* @return Illuminate\Support\Collection
*/
public function collection(array $attributes)
{
if ($this->hasSubclasses()) {
return $this->normalizeSubclassCollection($attributes);
}
}
In the normalizeSubclassCollection()
method we are going to need to instantiate a new Collection
and return it from the method:
/**
* Normalize a subclass collection
*
* @param array $attributes
* @return Illuminate\Support\Collection
*/
private function normalizeSubclassCollection($attributes)
{
$collection = new Collection;
return $collection;
}
Here I’m using the Collection
class from Illuminate\Support
as we are already pulling in that dependency so there is no point in reinventing the wheel.
In much the same way we used the root()
method to return the model root
, we also need to get the collection_root
:
/**
* Get the collection root of the entity
*
* @return string
*/
private function collectionRoot()
{
if ($this->collection_root) {
return $this->collection_root;
}
if (isset($options['collection_root'])) {
return $this->collection_root = $options['collection_root'];
}
return $this->collection_root = $this->model->serializableOptions()['collection_root'];
}
This code is basically the same as the root()
method.
Now back in the method we can iterate through the items in the response:
foreach ($attributes[(string) $this->collectionRoot()] as $key => $value) {
}
Finally we can iterate through the entities to instantiate them and add them to the collection.
A typical subclass collection response is going to look like this (taken from the documentation):
{
"parties": {
"person": [
{
"id": "100",
"firstName": "Eric",
"lastName": "Schmidt",
"createdOn": "2011-09-14T15:22:01Z",
"updatedOn": "2011-12-14T10:45:46Z"
},
{
"id": "101",
"firstName": "Larry ",
"lastName": "Page",
"createdOn": "2011-09-14T15:22:01Z",
"updatedOn": "2011-11-15T10:50:48Z"
}
],
"organisation": {
"id": "50",
"name": "Google Inc",
"createdOn": "2011-09-14T15:22:01Z",
"updatedOn": "2011-12-14T10:45:46Z"
}
}
}
So when there are multiple entities, in this case under person
we’re going to see an indexed array. However if there is only one entity, in this case under organisation
, we are going to see an associative array.
First we will check to see if the $value
is an associative array and if it is, we can pass it straight to the createNewModelInstance()
method:
if ($this->isAssociativeArray($value)) {
$collection[] = $this->createNewModelInstance($key, $value);
}
To check to see if the $value
is an associative array we can use this private method:
/**
* Check to see if the array is associative
*
* @param array $array
* @return bool
*/
private function isAssociativeArray($array)
{
return (bool) count(array_filter(array_keys($array), 'is_string'));
}
If the $value
is not an associative array we can just iterate through the array and pass each entity to the createNewModelInstance()
method:
else {
foreach ($value as $attributes) {
$collection[] = $this->createNewModelInstance($key, $attributes);
}
}
Now that we have the Normalizer
class set up for the Party
model we can insert it just after we receive the raw response from the API.
Update the all()
method to look like this:
/**
* Return all entities of the current model
*
* @param array $params
* @return array
*/
public function all(array $params = [])
{
$endpoint = '/api/'.$this->queryableOptions()->plural();
$response = $this->connection->get($endpoint, $params)->json();
$normalizer = new Normalizer($this);
return $normalizer->collection($response);
}
And the find()
method to look like this:
/**
* Find a single entity by it's id
*
* @param int $id
* @return array
*/
public function find($id)
{
$endpoint = '/api/'.$this->queryableOptions()->singular().'/'.$id;
$response = $this->connection->get($endpoint)->json();
$normalizer = new Normalizer($this);
return $normalizer->model($response);
}
In both of these updated methods I’m creating a new instance of the Normalizer
class and injecting it with the current model instance.
Next I pass the $response
to the appropriate method and return the returned value from either the model()
or collection()
method.
As I mentioned last week, I’m going to keep the Normalizer
class pretty light on tests and instead write tests for each model instance. The documentation provides us with stub responses, so we can just use those stubs to test the SDK is working correctly.
Create a new file called PartyTest.php
and copy the following code:
use Mockery as m;
use PhilipBrown\CapsuleCRM\Party;
class PartyTest extends PHPUnit_Framework_TestCase
{
/** @var PhilipBrown\CapsuleCRM\Connection */
private $connection;
/** @var PhilipBrown\CapsuleCRM\Party */
private $party;
/** @var Guzzle\Http\Message\Response */
private $message;
public function setUp()
{
$this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
$this->party = new Party($this->connection);
$this->message = m::mock("Guzzle\Http\Message\Response");
}
}
For this test I’m going to set up an instance of Party
and I’m going to mock the Connection
and the Response
from Guzzle.
In order to test that the Normalizer
class is doing its job correctly we need to save a copy of the stub responses from the CapsuleCRM documentation. I’ve saved these files under tests/stubs
.
The first test I’m going to write will be to find a single party instance:
/** @test */
public function find_party_by_id()
{
$response = file_get_contents(dirname(__FILE__).'/stubs/party.json');
$this->message->shouldReceive('json')->andReturn(json_decode($response, true));
$this->connection->shouldReceive('get')->andReturn($this->message);
$party = $this->party->find(100);
$this->assertInstanceOf('PhilipBrown\CapsuleCRM\Person', $party);
$this->assertEquals('100', $party->id);
$this->assertEquals('Eric', $party->first_name);
$this->assertEquals('Schmidt', $party->last_name);
$this->assertEquals('2011-09-14T15:22:01Z', $party->created_on);
$this->assertEquals('2011-12-14T10:45:46Z', $party->updated_on);
}
In this test I’m grabbing a copy of the stub JSON response and setting up the mock to return it when the get()
method is called.
Next I can assert that the correct model instance is created and the the properties have been set.
To test that collections are being normalised correctly we can basically use the same test, but written to assert a Collection
instance is being returned and the correct model instances have been created:
/** @test */
public function find_all_parties()
{
$response = file_get_contents(dirname(__FILE__).'/stubs/parties.json');
$this->message->shouldReceive('json')->andReturn(json_decode($response, true));
$this->connection->shouldReceive('get')->andReturn($this->message);
$collection = $this->party->all();
$this->assertInstanceOf('Illuminate\Support\Collection', $collection);
$this->assertTrue(count($collection) == 3);
$this->assertInstanceOf('PhilipBrown\CapsuleCRM\Person', $collection[0]);
$this->assertEquals('100', $collection[0]->id);
$this->assertEquals('Eric', $collection[0]->first_name);
$this->assertEquals('Schmidt', $collection[0]->last_name);
$this->assertEquals('2011-09-14T15:22:01Z', $collection[0]->created_on);
$this->assertEquals('2011-12-14T10:45:46Z', $collection[0]->updated_on);
$this->assertInstanceOf('PhilipBrown\CapsuleCRM\Person', $collection[1]);
$this->assertEquals('101', $collection[1]->id);
$this->assertEquals('Larry', $collection[1]->first_name);
$this->assertEquals('Page', $collection[1]->last_name);
$this->assertEquals('2011-09-14T15:22:01Z', $collection[1]->created_on);
$this->assertEquals('2011-11-15T10:50:48Z', $collection[1]->updated_on);
$this->assertInstanceOf('PhilipBrown\CapsuleCRM\Organisation', $collection[2]);
$this->assertEquals('50', $collection[2]->id);
$this->assertEquals('Google Inc', $collection[2]->name);
$this->assertEquals('2011-09-14T15:22:01Z', $collection[2]->created_on);
$this->assertEquals('2011-12-14T10:45:46Z', $collection[2]->updated_on);
}
The fact that the CapsuleCRM API uses child and parent classes in it’s API makes writing an Active Record SDK implementation a little bit more tricky. Fortunately, today we’ve tackled the most difficult part of this process.
I don’t think the code in today’s tutorial is particularly beautiful. It’s actually quite fragile because it is strictly following the implicit rules of the API. If the API were to change, this normalising process would likely stop working.
However, there isn’t always a beautiful solution to the problems you face. It’s often better to get a rough working version first, and then take a step back and see how you can simplify the design.