Aug 20, 2014
Table of contents:
One of the most important aspects of creating an API SDK is the ability to effectively query the underlying API in order to allow the developer to retrieve resources.
So far in this series we’ve looked at setting up the HTTP Client using Guzzle and then creating the abstract model that will form the foundation of each individual model implementation.
This week we’re going to look at adding the ability for each model instance to query their related API endpoint.
The first model that we’ll be working with will be the Party
model that we set up last week. If you are following along with the Capsule CRM API documentation you will want to reference this page.
Last week we simply created the Party.php
file and extended the abstract Model
.
The next thing we need to do is to create the __construct()
method and inject an instance of the Connection
object that we created in this tutorial.
Your Party
model should look like this:
<?php namespace PhilipBrown\CapsuleCRM;
class Party extends Model
{
/**
* Create a new instance of the Party model
*
* @param PhilipBrown\CapsuleCRM\Connection $connection
* @return void
*/
public function __construct(Connection $connection)
{
parent::__construct($connection);
}
}
The first thing I’m going to do will be to create a Querying
directory to hold the files for querying the API. There are going to be a number of small files that enable querying the API and so I think it makes sense from a code organisation perspective to make this separation.
This means, instead of creating a monolithic abstract Model
class that holds all responsibility for managing the object’s properties, querying the API and persisting objects to storage, I’m going to be splitting each of these responsibilities into their own separated modules.
Hopefully splitting the project into these subdirectories will make understanding this package at first blush a lot easier.
When querying the API there are basically two operations that take place.
First we can query based upon the resource id. For example, if we wanted to find a user with an id of 1.
Secondly we can find all resources that match set parameters. For example, we could find all users with a certain name.
Instead of lumping these two operations into one class, I’m going to split them into two separate classes. This means if a certain model should only be able to find all and not find one, we don’t have to do any weird jiggery-pokery to stub out methods that are inherited.
To implement these two different operations I’m going to create two separate traits that can be simply included into a model class to enable the ability to query in one, or both ways.
So the first thing to do will be to create the two trait classes:
<?php namespace PhilipBrown\CapsuleCRM\Querying;
trait FindAll
{
}
<?php namespace PhilipBrown\CapsuleCRM\Querying;
trait FindOne
{
}
The first bit of functionality we need to tackle is the ability to infer the endpoint from the model. If you remember back a couple of weeks ago, we implemented this by using PHP’s reflection capabilities to get the class name and morph it into the correct form.
This will allow us to take an object like User
and infer that the API endpoint should be users
even when we don’t know what object we are currently working with at runtime. If you want to read more about how we implemented this functionality, take a look at this tutorial.
However, if we take a look at the API documentation, we can see that sometimes we need to use the plural name of the resource and other times we need to use the singular name of the resource.
After looking at a lot of API’s over the years, one thing that becomes apparent is, API’s often have these weird idiosyncrasies or inconsistencies.
For this package I’m going to implement a general rule of if we’re using the FindOne
trait we’ll use the singular resource name, and if we’re using the FindAll
trait we’ll use the plural resource name.
In circumstances where the resource in question does not follow this pattern we’ll override this convention with a property on the model.
Before we jump into the code, first I will give you a brief overview of how this is going to work.
Within the FindOne
and FindAll
traits I want to be able to call a method that can be chained to provide the correct endpoint for the current model.
For example the FindOne
trait will, by default, look for the singular version of the resource name. The following code will be used to get the endpoint.
$model->queryableOptions()->singular();
However, if this particular resource has an override, the code above should be overwritten with the value that has been set in the model instance.
This means that by default the model will return the endpoint to follow the convention, but we can very easily override it by providing a configuration item in the model instance.
If any of that didn’t make sense, don’t worry, read on as I’m sure it will all fall into place when you see the code!
The first thing I will do will be to write a couple of tests for how the functionality will work:
use Mockery as m;
class QueryingTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
$this->model = new ModelStub($connection);
}
public function testTheSingularQueryableName()
{
$this->assertEquals(
"modelstub",
$this->model->queryableOptions()->singular()
);
}
public function testThePluralQueryableName()
{
$this->assertEquals(
"the_plural_name",
$this->model->queryableOptions()->plural()
);
}
}
As with the previous tutorials we first write a setUp()
method that will instantiate a new instance of the ModelStub
object so we don’t have to repeat ourselves for each test.
Next we can write two tests to assert this functionality is working correctly.
The first step will expect that the default lowercase, singular version of the model will be returned.
The second step will expect that we have overwritten the convention to provide a different endpoint.
If we now run these tests we will see a whole lot of red.
Time to implement the code!
The first thing I will do will be to add a property and a method to the abstract Model
class as a foundation for this functionality. This will mean the property is always set to the right type (an array) and there will always be the method required to return it.
Add the following property:
/**
* The model's queryable options
*
* @var array
*/
protected $queryableOptions = [];
And the following method:
/**
* Return the queryable options
*
* @return array
*/
public function getQueryableOptions()
{
return $this->queryableOptions;
}
We don’t need to be able to set this array because each model instance will either have it defined in the class, or will just ignore it completely.
Next we can add the property to the ModelStub
class to satisfy the requirements of the test assertion:
protected $queryableOptions = ['plural' => 'the_plural_name'];
The next thing we will do will be to create a Configuration
trait that will provide the queryableOptions()
method. This file will be nested under the Querying
directory:
<?php namespace PhilipBrown\CapsuleCRM\Querying;
trait Configuration
{
/**
* Return an instance of the Options object
*
* @return PhilipBrown\CapsuleCRM\Querying\Options
*/
public function queryableOptions()
{
return new Options($this);
}
}
This method will create a new Options
object that accepts an instance of the current model as it’s only dependency.
The logic around getting the default endpoints and merging with any override options within the model instance will be dealt with within a new Options
object. Again this object will be nested under the Querying
directory.
Create a new file called Options.php
under the Querying
directory:
<?php namespace PhilipBrown\CapsuleCRM\Querying;
class Options
{
/**
* Create a new Options object
*
* @param PhilipBrown\CapsuleCRM\Model
* @return void
*/
public function __construct(Model $model)
{
}
/**
* Return the singular name of the model
*
* @return string
*/
public function singular()
{
}
/**
* Return the plural name of the model
*
* @return string
*/
public function plural()
{
}
}
As you can see, we inject an instance of the current model as a dependency to the Options
object. We also provide two methods to return the singular or plural form of the resource name.
Within the __construct()
method we can set up the default convention as a base array:
/**
* Create a new Options object
*
* @param PhilipBrown\CapsuleCRM\Model
* @return void
*/
public function __construct(Model $model)
{
$base = [
'plural' => $model->base()->lowercase()->plural(),
'singular' => $model->base()->lowercase()->singular()
];
}
This is simply using the code from a couple of weeks ago to infer the resource name by reflection.
Next we can use these defaults and merge in any overriding configuration options that have been set on the model:
$this->options = array_merge($base, $model->getQueryableOptions());
We then set the merged array as a property on the object. Don’t forget to add this property definition to the class:
/**
* The merged array of options
*
* @var array
*/
protected $options;
Finally we can implement the singular()
and plural()
methods to return the value from the $options
array:
/**
* Return the singular name of the model
*
* @return string
*/
public function singular()
{
return $this->options['singular'];
}
/**
* Return the plural name of the model
*
* @return string
*/
public function plural()
{
return $this->options['plural'];
}
By default these values will be the default values that follow the convention of the package. However, if the model class has an override, that value will be returned instead.
Now if you run the tests from earlier you should see them all pass.
You might be thinking this is a lot of work when in reality we could just set the api endpoints on each model. I would tend to agree with that statement, but it wouldn’t make for a very interesting tutorial.
In this tutorial we’ve looked at a couple of important concepts.
First we are encapsulating the ability to query the API as traits. This means we can optionally add the functionality to each model on a case-by-case basis. I’m not 100% sure that this is the best use of traits to be honest, but you only find the limitations of a technique by exploring the boundaries.
Secondly we created an Options
object to encapsulate the logic around returning the model endpoint or an override default. I think this is important because we’ve created a dedicated class to perform this action, rather than forcing it into one of the other classes. I think getting into the true mindset of the Single responsibility principle can take some getting used to.
Next week we will look at implementing the query traits to pull data from the API.