Oct 08, 2014
Table of contents:
Last week we started looking at adding the functionality to persist entities to the API.
Part of the beauty of the Active Record pattern is how easy it is to persist records to the data store. This is because each model object should have everything it needs to read, write, update and delete.
However, in order to send our PHP model objects to the API via an HTTP request, we need to serialise them into a common format. In this case to JSON.
Unfortunately, serialising the model objects isn’t as easy as simply throwing it in to the json_encode()
function.
So the focus of today’s tutorial will be around how to serialise the model objects so we can send them to the API.
Whenever we make an API request, we need a way of transforming the current model object into JSON.
However, it’s not just about transforming the properties of the model object into JSON, we also need to account for how the API expects the JSON to be formatted.
What’s more, with this being an Active Record PHP SDK we will also need to be able to include embedded objects and relationships.
When faced with a problem like this, it’s often beneficial to look from the outside in.
Ideally, the process for transforming a model object from PHP to JSON should be as simple as:
$person->toJson();
To the outside world, all model objects should have this very simple public API. The actual implementation of how the object is getting transformed to JSON should not be a concern.
So step 1 is to add a toJson()
method to each model object.
Secondly, there is going to be quite a bit of logic involved with transforming a model object into JSON as we’re going to have to account for a couple of scenarios. To deal with this situation we can abstract the serialisation code into it’s own object so we don’t have to create one big monolithic abstract Model
class.
So step 2 will be to create a Serializer
object that can handle serialising any of the model objects from this package.
So hopefully that make sense. If not, keep reading as I’m sure it will start to fall into place.
The first thing we need to do is to add the toJson()
method to each of the models. It is the toJson()
method that will invoke the serialisation process.
Instead of adding this method to the abstract Model
class I’m going to add it to the Serializable
trait we looked at in part 9 of this series.
If you have been following along with this series you might remember that we implemented a serializableOptions()
method on the Serializable
trait. This allows us to specify some default configuration options for how models should be serialised, but it will also merge in any overrides that we specify in the model class.
The first thing I’m going to do will be to add some extra defaults to the serialisable options:
/**
* Set the serializable options array
*
* @return void
*/
private function setSerializableOptionsArray()
{
$this->serializableOptions = [
'root' => $this->base()->lowercase()->singular(),
'collection_root' => $this->base()->lowercase()->plural(),
'include_root' => true,
'additional_methods' => []
];
}
I’ll explain these extra options when we come to implement them.
As I mentioned above, we’re going to keep all of the logic of how models should be serialised in a Serializer
object. This is to prevent the abstract Model
class from getting too big. It will also make it a lot easier to test.
Next we need to add a private
method to the Serializable
trait to instantiate a new instance of the Serializer
class:
/**
* Create a new Serializer object
*
* @return Serializer
*/
private function serializer()
{
$this->setSerializableOptionsArray();
return new Serializer($this->serializableOptions());
}
You will notice that I’m passing in the options array so we can specify how this model should be serialised.
Finally we can create the toJson()
method:
/**
* Serialize the current object to JSON
*
* @return string
*/
public function toJson()
{
return $this->serializer()->serialize($this);
}
This method will instantiate a new Serializer
instance through the private``serializer()
method.
We will then call the serialize()
method and pass an instance of $this
.
The $this
variable in this context relates to the current model object. This effectively means it passes itself into the method call.
The next thing we need to do will be to create the Serializer
class.
As we’ve already seen from the Serializable
trait, this class should accept an array
of options:
<?php namespace PhilipBrown\CapsuleCRM;
class Serializer
{
/**
* @var array
*/
private $options;
/**
* Create a new Serializer
*
* @param array $options
* @return void
*/
public function __construct(array $options)
{
$this->options = $options;
}
}
We also need a single public``serialize()
that should accept an instance of Model
:
/**
* Serialize a model instance
*
* @param Model $model
* @return string
*/
public function serialize(Model $model)
{
$this->model = $model;
}
We’re going to need the $model
object in the other methods of this class, so add it as a property of the class.
One of the big differences between certain models of the CapsuleCRM API is, some have a root, whilst others don’t.
For example, if you look at the documentation for the person resource, the API is expecting the JSON body to look like this:
{
"person": {
"contacts": {
"address": {
"type": "Office",
"street": "1600 Amphitheatre Parkway",
"city": "Mountain View",
"state": "CA",
"zip": "94043",
"country": "United States"
},
"email": {
"type": "Home",
"emailAddress": "e.schmidt@google.com"
},
"phone": {
"type": "Mobile",
"phoneNumber": "+1 888 555555"
},
"website": {
"type": "work",
"webService": "URL",
"webAddress": "www.google.com"
}
},
"title": "Mr",
"firstName": "Eric",
"lastName": "Schmidt",
"jobTitle": "Chairman",
"organisationName": "Google Inc",
"about": "A comment here"
}
}
In this instance the JSON body is nested under the person
key, however, not all of the models should be serialised like this.
In the Serializable
trait, one of the default options we added was:
'include_root' => true
By default we’re going to assume that the root should be included. For models where it should not be included, we can simply add the following to the configuration array:
'include_root' => false
Back in the Serializer
class we can add two private
methods under the serialise()
method:
/**
* Check to see if the current model should include the root
*
* @return bool
*/
private function includeRoot()
{
return $this->options['include_root'];
}
/**
* Return the root of the model
*
* @return string
*/
private function root()
{
if (isset($this->options['root'])) {
return (string) $this->options['root'];
}
return (string) $this->model->base()->lowercase()->singular()->camelcase();
}
The first method is simply returning the value of the $this->options['include_root'];
property. This will be true
by default.
The second method will see if the root
property has been set on the $this->options
array, and if it has it will return it.
However if it has not been set, we can fall back to the lowercase, singular, camel case version of the model name.
This will allow us to use the sensible default, but also override it for certain models that require weird ‘root’ properties.
The main difference between how models should be serialised is determined by if the root
should be included or not.
Now that we’ve added the ability to check to see if the root
should or shouldn’t be included, we can finish off the serialize()
method:
/**
* Serialize a model instance
*
* @param Model $model
* @return string
*/
public function serialize(Model $model)
{
$this->model = $model;
if ($this->includeRoot()) {
return $this->serializeWithRoot();
}
return $this->serializeWithoutRoot();
}
In this method I’m simply calling one of two methods depending on whether the root
should be included.
Those two methods are as follows:
/**
* Serialize the model with the root
*
* @return string
*/
private function serializeWithRoot()
{
return json_encode([$this->root() => $this->buildAttributesArray()]);
}
/**
* Serialize the model without the root
*
* @return string
*/
private function serializeWithoutRoot()
{
return json_encode($this->buildAttributesArray());
}
As you can see, these two methods are very similar, we are simply passing the return value of $this->buildAttributesArray()
into the json_encode()
function.
Next we need to grab the attributes from the $model
instance so we can transform it into JSON.
Due to the fact that this is an Active Record implementation, it won’t be as straightforward as simply grabbing the $attributes
array from the model.
A couple of weeks ago I decided that all model properties should be in snake case, rather than camel case. This was a personal preference, but the Capsule CRM requires camel case properties so the first thing to do will be transform them:
/**
* Build the attributes array
*
* @return array
*/
private function buildAttributesArray()
{
return Helper::toCamelCase($this->cleanedAttributes());
}
In this method I’m simply passing an array of attributes to the toCamelCase()
static method on the Helper
class.
At the minute we’re only concerned with getting this process working. However in the coming weeks we’re going to need to add functionality to this class to serialise relationships and embedded objects.
The full serialisation process will use the following two method to grab and clean the attributes that should be sent to the API:
/**
* Return the cleaned attributes
*
* @return array
*/
private function cleanedAttributes()
{
return $this->attributes();
}
/**
* Get and format the model attributes
*
* @return array
*/
private function attributes()
{
return $this->model->attributes()
}
As you can see, these two methods are bit redundant at the minute, but it will be easier to finish off this process at a later date if we don’t try and cram everything into one method.
As with the process of normalising API responses into PHP objects, I’m going to keep the tests for the Serializer
class fairly minimal. I think there is a lot more value in simply testing each resource to ensure that the process is working correctly over all.
Each resource of the API has sample JSON requests that we can copy and store as stub files to compare the output of the serialisation process. As I’ve mentioned previously, when testing an API we don’t want to actually hit the API end points. These stub responses act as the contract we must meet in order to know that our requests will be successful.
I won’t go through all of the tests because they’re basically exactly the same. If you would like to see each test file individually, all of the code is on GitHub
One thing to point out is that the Person
and the Organisation
tests won’t pass with the sample JSON from the API documentation.
If you have a look at the documentation for creating a new Person
object, you will notice that the suggested JSON request also includes an embedded Contacts
object.
We haven’t created this object yet and so the JSON from our serialiser won’t match the JSON in this stub.
To get around this problem we can simply unset()
the contacts
key of the sample request to ignore it for now. When we come to add the embedded Contact
object back in, we can remove this hack.
The test for the Person
class looks like this:
/** @test */
public function should_serialize_model()
{
$person = new Person($this->connection, [
'title' => 'Mr',
'first_name' => 'Eric',
'last_name' => 'Schmidt',
'job_title' => 'Chairman',
'organisation_name' => 'Google Inc',
'about' => 'A comment here'
]);
$stub = json_decode(file_get_contents(dirname(__FILE__).'/stubs/post/person.json'), true);
unset($stub['contacts'])
$this->assertEquals(json_encode($stub), $person->toJson());
}
During the assertion we’re checking that the two strings match. The sample stub is formatted nicely, whereas the JSON that the $person
object will produce won’t be. To solve this problem, simply json_decode()
the stub file and the json_encode()
it during the assertion.
You will need to do exactly the same for the Organisation
model.
As with normalising responses from the API, serialising model objects is also a little bit more complicated than you would expect from the surface.
Each model object will require a slightly different serialisation process. This means it’s pretty hard to find a one-size-fits-all solution.
In today’s tutorial we’ve covered the basics of serialising the model objects. Next week we will continue to build out this functionality so we can simply call the toJson()
method on any object to serialise it to JSON.