cult3

How to create an Active Record style PHP SDK Part 10

Sep 10, 2014

Table of contents:

  1. How will the normalising process work?
  2. Writing the Normalizer class
  3. Conclusion

Making requests and receiving responses from a third-party API is pretty easy. By default most APIs follow the conventions of REST and use a common format of either JSON or XML to send and receive data.

However once we have accepted a response from the API, we need a way of parsing the payload into objects we can work with within our code.

As I mentioned last week, we need a way of “normalising” the responses from the API so we can transform them into the correct model objects.

This normalising functionality can be encapsulated into a dedicated class that has the sole responsibility of transforming payloads into objects. This means we don’t have to dump this responsibility into a monolithic Model class.

Today we will look at creating the Normalizer class.

How will the normalising process work?

A couple of weeks ago we implemented the code to query the API. Whenever we make a request to the API we will be returned a JSON response.

However we don’t want to pass this raw JSON response back to the developer to work with. Instead we need to transform the response into either a single model instance or a collection of model instances depending on the response.

So we need to write a class that will accept a JSON response and be able to return either a single instance or a collection of instances of the correct type based upon the JSON payload.

Writing the Normalizer class

The Normalizer class is going to accept a JSON payload and return either a single instance or a collection of instances of the correct model type. Instead of creating fake JSON responses to test the Normalizer class, I’m going to keep the tests pretty light for this class because I can use the stub responses from the API documentation to test each model instance.

The Normalizer class will need to accept an instance of the current model in through the constructor. This is so we know what type of model we are working with.

We will also need to define two public methods for model() and collection() that will accept a JSON response and return the appropriate instance or collection of models.

The first thing I will do will be to create the Normalizer.php file:

<?php namespace PhilipBrown\CapsuleCRM;

class Normalizer
{
}

The Constructor

Next I will write the __construct() method. As I mentioned above we will need to inject an instance of the current Model so we know what type of model we are working with. In a future tutorial we will also require an array of optional $options, but for now we can just set this an array by default:

/**
 * Create a new Normalizer instance
 *
 * @param PhilipBrown\CapsuleCRM\Model $model
 * @param array $options
 * @return void
 */
public function __construct(Model $model, array $options = [])
{
    $this->model = $model;
    $this->options = $options;
}

Don’t forget to set the class properties too:

/**
 * The Model instance
 *
 * @var PhilipBrown\CapsuleCRM\Model
 */
protected $model;

/**
 * The options array
 *
 * @var array
 */
protected $options;

The public methods

Next we can define the two public methods of the class for normalising either a single entity or a collection of entities.

The model() method looks like this:

/**
 * Normalize a single model
 *
 * @param array $attributes
 * @return PhilipBrown\CapsuleCRM\Model
 */
public function model(array $attributes) {}

And the collection() method looks like this:

/**
 * Normalize a collection of models
 *
 * @param array $attributes
 * @return Illuminate\Support\Collection
 */
public function collection(array $attributes) {}

Both of these methods accept the JSON payload as a generic array of attributes.

Writing the tests

As I mentioned above, I’m going to keep the tests for this class pretty light. Instead of creating fake stub JSON responses, I’m only going to test the public API of this class. I can test that the internals are working correctly by testing each individual model with the JSON response that have been provided by the documentation.

Firstly I will test to ensure that the Normalizer class should be instantiated with both arguments of the correct type:

/** @test */
public function should_require_model()
{
    $this->setExpectedException('Exception');

    $normalizer = new Normalizer("", []);
}

/** @test */
public function should_require_options_array()
{
    $this->setExpectedException('Exception');

    $normalizer = new Normalizer($this->model, "");
}

Next I will test to ensure that both public methods should accept an array of attributes:

/** @test */
public function model_method_should_require_attributes_array()
{
    $this->setExpectedException('Exception');

    $this->normalizer->model();
}

/** @test */
public function collection_should_require_attributes_array()
{
    $this->setExpectedException('Exception');

    $this->normalizer->collection();
}

The full test class looks like this:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Model;
use PhilipBrown\CapsuleCRM\Connection;
use PhilipBrown\CapsuleCRM\Normalizer;

class NormalizerTest extends PHPUnit_Framework_TestCase
{
    /** @test PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @test PhilipBrown\CapsuleCRM\Model */
    private $model;

    /** @test PhilipBrown\CapsuleCRM\Normalizer */
    private $normalizer;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new NormalizeModelStub($this->connection);
        $this->normalizer = new Normalizer($this->model);
    }

    /** @test */
    public function should_require_model()
    {
        $this->setExpectedException("Exception");

        $normalizer = new Normalizer("", []);
    }

    /** @test */
    public function should_require_options_array()
    {
        $this->setExpectedException('Exception');

        $normalizer = new Normalizer($this->model, "");
    }

    /** @test */
    public function model_method_should_require_attributes_array()
    {
        $this->setExpectedException("Exception");

        $this->normalizer->model();
    }

    /** @test */
    public function collection_should_require_attributes_array()
    {
        $this->setExpectedException("Exception");

        $this->normalizer->collection();
    }
}

class NormalizeModelStub extends Model
{
    public function __construct(Connection $connection, $attributes = [])
    {
        parent::__construct($connection);

        $this->fill($attributes);
    }
}

Conclusion

Today we looked at the foundation of the Normalizer class. This class has a lot going on, and so it’s going to take more than one tutorial to explain how it works.

I think the important thing to take away from this tutorial is, when faced with a complicated process such as transforming JSON payloads into model instances, don’t try and add it to the existing class. You should always look at how you can break out functionality into it’s own object.

This is beneficial in two big ways. Firstly, you prevent the original class from acquiring functionality that it should not be responsible for. Secondly, testing 2 or 3 smaller classes is always easier than testing 1 big class!

Next week we will look at how to normalise the tricky ancestor models that are very important to the CapsuleCRM API.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.