cult3

How to create an Active Record style PHP SDK Part 13

Oct 01, 2014

Table of contents:

  1. What are the Active Record methods of persistence?
  2. The Storable trait
  3. Generating the correct endpoint
  4. The Configuration trait
  5. Creating or Updating the entity
  6. Making the requests
  7. Conclusion

Over the last couple of weeks we’ve looked at adding the ability to query an API using the Active Record Pattern. The Active Record Pattern dictates that the model object should have the ability to query the data store. We implemented this functionality by providing a find() and an all() for retrieving single entities or a collection of entities respectively.

Now that we’ve finished off looking at querying, we can turn our attention to persisting entities to the API.

Once again the Active Record Pattern dictates that the model object should be capable of creating, updating and deleting from the data store.

In today’s tutorial we’re going to lay the foundation for building out the persistence functionality of this Active Record style PHP SDK.

What are the Active Record methods of persistence?

If you have ever used an ORM, such as Laravel’s Eloquent, you will probably already be familiar with how the Active Record pattern should work.

The basic premise of the pattern is that model objects have the capability of persisting to the data store. This means each model object will have methods that are associated with each of the typical CRUD actions.

For example, you might have the following Person object:

$person = new Person();

The Person class should have methods to create, save, update and delete from the data store.

Interacting with the data store through a model object would typically look like this:

// Create
$person = $person->create(["name" => "John"]);

// Save
$person->name = "Paul";
$person->save();

// Update
$person->update(["name" => "George"]);

// Delete
$person->delete();

Each time one of these methods is called on the object a request will be made to the data store. Usually this would be to the database, but in our case it will be an HTTP request to the API.

The Storable trait

As I’ve already shown a couple of times in this series so far, in order to provide this public API of persistable methods, we can use a trait.

Using a trait gives us the benefit of being able to optionally add the methods to each model instance.

I will also be splitting the methods into two traits, one for adding or updating an entity, and one for deleting entities.

Create a new directory under src called Persistance

Next create a new file in the Persistance directory called Storable and copy the following code:

<?php namespace PhilipBrown\CapsuleCRM\Persistance;

trait Storable
{
    /**
     * Create a new entity
     *
     * @param array $attributes
     * @return Model
     */
    public function create(array $attributes)
    {
    }

    /**
     * Update an existing entity
     *
     * @param array $attributes
     * @return Model
     */
    public function update(array $attributes)
    {
    }

    /**
     * Save the current entity
     *
     * @return bool
     */
    public function save()
    {
    }
}

Now whenever we add the Storable trait to a model class we will have the methods above made available as part of the model’s public API.

Generating the correct endpoint

A RESTful API will have specific endpoints for each resource and each available HTTP method of that resource. As with when we were querying the API, we need a way of generating these endpoints at runtime so that we can send the request to the right place.

This time it is slightly more complicated to generate the endpoints because we also need to include the current model’s id as part of the request when updating or deleting from the API.

To do this we can create a new object that accepts an instance of the current model as a dependency. We can then provide methods for create(), update() and delete() that will return the generated endpoints.

Create a new test file called PersistableOptionsTest and copy the following set up code:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Person;
use PhilipBrown\CapsuleCRM\Persistance\Options;

class PersistableOptionsTest extends PHPUnit_Framework_TestCase
{
    /** @var Options */
    private $options;

    public function setUp()
    {
        $this->options = new Options(
            new Person(m::mock("PhilipBrown\CapsuleCRM\Connection"), [
                "id" => 123,
            ])
        );
    }
}

In the setUp() method I’m injecting a new instance of the Person model into a new Options class that will sit under the Persistance namespace.

Using the Options object we should be able to generate the endpoints in the following ways:

/** @test */
public function should_generate_create_endpoint()
{
    $this->assertEquals('people', $this->options->create());
}

/** @test */
public function should_generate_update_endpoint()
{
    $this->assertEquals('person/123', $this->options->update());
}

/** @test */
public function should_generate_delete_endpoint()
{
    $this->assertEquals('person/123', $this->options->delete());
}

If you run those tests they will fail because we haven’t wrote the Options class yet.

Next create a new file for the Options class under the Persistance namespace and copy the following code:

<?php namespace PhilipBrown\CapsuleCRM\Persistance;

use PhilipBrown\CapsuleCRM\Model;

class Options
{
    /**
     * @var Model
     */
    private $model;

    /**
     * Create a new Options object
     *
     * @param PhilipBrown\CapsuleCRM\Model
     * @return void
     */
    public function __construct(Model $model)
    {
        $this->model = $model;
    }
}

As you can see this class requires an instance of Model to be injected through the constructor.

Next we can satisfy the tests we wrote earlier with the following methods:

/**
 * Generate the create endpoint
 *
 * @return string
 */
public function create()
{
    return $this->model->base()->lowercase()->plural();
}

/**
 * Generate the update endpoint
 *
 * @return string
 */
public function update()
{
    return $this->model->base()->lowercase()->singular().'/'.$this->model->id;
}

/**
 * Generate the delete endpoint
 *
 * @return string
 */
public function delete()
{
    return $this->model->base()->lowercase()->singular().'/'.$this->model->id;
}

In each case we use the model’s reflection to convert the model name into the appropriate form. For the update() and delete() methods we can also append the $this->model->id to the end of the endpoint.

Now if you run those tests again, they should all pass.

We’ve satisfied the requirements of the tests, but we haven’t fully satisfied the requirements of the API. The returned values from the create(), update(), and delete() methods are a good starting point, but we need a way of overwriting them for certain models.

Update the __construct() method to accept an array of Options:

/**
 * @var Model
 */
private $model;

/**
 * @var array
 */
private $options;

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

Next update each of the methods to first check to see if an option override has been set and return it if it has:

/**
 * Generate the create endpoint
 *
 * @return string
 */
public function create()
{
    if (isset($this->options['create'])) return $this->options['create']();

    return $this->model->base()->lowercase()->plural();
}

/**
 * Generate the update endpoint
 *
 * @return string
 */
public function update()
{
    if (isset($this->options['update'])) return $this->options['update']();

    return $this->model->base()->lowercase()->singular().'/'.$this->model->id;
}

/**
 * Generate the delete endpoint
 *
 * @return string
 */
public function delete()
{
    if (isset($this->options['delete'])) return $this->options['delete']();

    return $this->model->base()->lowercase()->singular().'/'.$this->model->id;
}

Each of these methods will first check to see if an option has been set. If the option has been set, we can simply return it.

You will notice that we’re calling:

$this->options["delete"]();

This means we’re invoking a function. To set an override in the model class we can create an array of anonymous functions (What are PHP Lambdas and Closures?.

An example of using an override in a model can be found in the Person class:

$this->persistableConfig = [
    "create" => function () {
        return "person";
    },
    "delete" => function () {
        return "party/$this->id";
    },
];

Finally we can add the $persistableConfig class property to the Model abstract class:

/**
 * The model's persistable config
 *
 * @var array
 */
protected $persistableConfig = [];

The Configuration trait

Once again, as with the Querying traits, we can provide a Configuration trait that will provide a method to instantiate the Options object.

<?php namespace PhilipBrown\CapsuleCRM\Persistance;

trait Configuration
{
    /**
     * Return an instance of the Options object
     *
     * @return PhilipBrown\CapsuleCRM\Persistance\Options
     */
    public function persistableConfig()
    {
        return new Options($this, $this->persistableConfig);
    }
}

This will provide the persistableConfig() method that will allow a model instance to get the appropriate endpoint for the given HTTP method.

Creating or Updating the entity

When using the create() method we are obviously creating a new record, and when we are using the update() method we are obviously updating an existing record.

However when we use the save() method, we have no idea whether the request should be sent to the create, or the update endpoint.

To solve this problem we can provide a method to check to see if this is a new record or not. We can determine this by checking to see if the current model has an id. Only existing records should have an id as it should be generated by the API.

/**
 * Is this a new entity?
 *
 * @return bool
 */
private function isNewEntity()
{
    return ! isset($this->attributes['id']);
}

We can also provide the opposite check by simply inverting the returned response from the isNewEntity() method:

/**
 * Is this an existing entity?
 *
 * @return bool
 */
private function isPersisted()
{
    return ! $this->isNewEntity();
}

Making the requests

Finally we can write two private methods for actually making the request. These two methods will use the Connection object to POST or PUT as appropriate.

/**
 * Create a new entity request
 *
 * @return Model
 */
private function createNewEntityRequest() {}

/**
 * Update an existing request
 *
 * @return Model
 */
private function updateExistingEntityRequest() {}

Conclusion

In order to make the request to the API we need:

  1. Access to the Connection
  2. The endpoint we need to hit
  3. The model serialised into JSON

We’ve already implemented 1 and 2, but we still need a way of serialising the model instances into JSON so they can be sent over the wire.

As you can probably guess, this isn’t going to be as simple as throwing the model object into the json_encode() function.

Next week we will look to see how we can use a dedicated class to serialise the model objects into an acceptable format for the CapsuleCRM API.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.