cult3

How to create an Active Record style PHP SDK Part 7

Aug 20, 2014

Table of contents:

  1. Working with the Party model
  2. Create the Querying directory
  3. Using traits to query
  4. Inferring the endpoint from the model
  5. Setting up the Queryable options
  6. Writing the tests
  7. Setting up the foundation
  8. Create the Configuration trait
  9. Creating the Options object
  10. Conclusion

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.

Working with the Party model

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);
    }
}

Create the Querying directory

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.

Using traits to query

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
{
}

Inferring the endpoint from the model

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.

Setting up the Queryable options

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!

Writing the tests

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!

Setting up the foundation

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'];

Create the Configuration trait

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.

Creating the Options object

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.

Conclusion

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.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.