cult3

How to create an Active Record style PHP SDK Part 12

Sep 24, 2014

Table of contents:

  1. The plan of action
  2. The Opportunity model
  3. The Kase and Task models
  4. The Task Model
  5. The User and Track models
  6. The History Model
  7. The Country and Currency models
  8. Conclusion

Last week we looked at implementing the process for normalising models with subclasses. The API we are working with has the concept of Person and Organisation entities as child entities to a parent Party entity. This makes normalising responses from the API slightly more tricky because we have to be able to dynamically determine how models should be created at runtime.

Fortunately the implementation of normalising subclasses was the hardest bit of this aspect of building the Active Record style SDK. Now we can concentrate on building out the querying functionality for the remaining API resources.

As I mentioned last week, instead of writing a lot of tests for the Normalizer class, I’m going to write tests for each Model instance instead.

So today we will walk through creating each remaining model class and then writing the code to enable querying of that particular API resource.

The plan of action

Before I get into the weeds of writing the code to finish off implementing the Normalizer class, first I will make a list of the models that I’m going to need to create and what type of querying should be enabled.

If we take a look at the developer documentation, we will see we need to implement the following models:

Opportunity

  • Find an opportunity
  • Find all opportunities

Kase

  • Find a kase
  • Find all kases

History

  • Find one

Task

  • Find a task
  • Find all tasks

User

  • List all users

Country

  • List all countries

Currency

  • List all currencies

Track

  • List all tracks

As you can see, not all of the remaining models should be able to find one and find all entities from the API. Each of the remaining resources also have slight nuances that we are going to have to account for.

We’ll take it one step at a time and work through implementing each one.

The Opportunity model

The first model we will look at will be the Opportunity model.

If we take a look at the documentation, we will see we need to implement both the find() and all() methods.

As I mentioned last week, when testing the ability to query the API, we don’t want to actually hit the API during our tests. Instead we can just mock the response so we only need to test our code.

The Opportunity stubs

So the first thing to do is to copy the example responses from the API documentation and save them into our stubs directory.

Create a new file called opportunity.json and copy the following JSON:

{
    "opportunity": {
        "id": "43",
        "name": "Consulting",
        "description": "Scope and design web site shopping cart",
        "partyId": "2",
        "currency": "GBP",
        "value": "500.00",
        "durationBasis": "DAY",
        "duration": "10",
        "expectedCloseDate": "2012-09-30T00:00:00Z",
        "milestoneId": "2",
        "milestone": "Bid",
        "probability": "50",
        "owner": "a.user",
        "createdOn": "2011-09-30T00:00:00Z",
        "updatedOn": "2011-09-30T00:00:00Z"
    }
}

And another one for opportunities.json:

{
    "opportunities": {
        "opportunity": [
            {
                "value": "500.00",
                "id": "43",
                "durationBasis": "DAY",
                "createdOn": "2011-09-30T00:00:00Z",
                "milestoneId": "2",
                "duration": "10",
                "currency": "GBP",
                "description": "Scope and design web site shopping cart",
                "name": "Consulting",
                "owner": "a.user",
                "milestone": "Bid",
                "updatedOn": "2011-09-30T00:00:00Z",
                "probability": "50",
                "expectedCloseDate": "2012-09-30T00:00:00Z",
                "partyId": "2"
            }
        ]
    }
}

The Opportunity model

Next we can create the Opportunity class. If you have a read of the documentation, you will see that the opportunity resource is pretty standard, so we’ve already implemented all the required functionality from the last couple of weeks.

Here is what the Opportunity class should look like:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\Findable;

class Opportunity extends Model
{
    use Findable;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = [
        "id",
        "name",
        "description",
        "party_id",
        "currency",
        "value",
        "duration_basis",
        "duration",
        "expected_close_date",
        "milestone_id",
        "milestone",
        "probability",
        "owner",
        "created_on",
        "updated_on",
    ];

    /**
     * The model's queryable options
     *
     * @var array
     */
    protected $queryableOptions = [
        "plural" => "opportunity",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

Firstly we can extend the abstract Model to inherit a lot of the functionality that we require. We can also implement the Findable and Serializable traits to add the functionality from the last couple of weeks.

If we have a look at the documentation we can see that the plural endpoint should be the singular name of the model. To set this in the model we can simply set the $queryableOptions property.

And finally we can set the standard __construct() method that should be present in all of our models.

The Opportunity tests

With the Opportunity model in place we can now write the tests to ensure that the Normalizer class is able to take the stub response and transform it into the correct model objects.

Create a new file called OpportunityTest.php and copy the following code:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Opportunity;

class OpportunityTest extends PHPUnit_Framework_TestCase
{
}

The first thing we will do will be to write a setUp() method so we don’t have to set the model up for each test:

/** @var PhilipBrown\CapsuleCRM\Connection */
private $connection;

/** @var PhilipBrown\CapsuleCRM\Opportunity */
private $opportunity;

/** @var Guzzle\Http\Message\Response */
private $message;

public function setUp()
{
    $this->connection = m::mock('PhilipBrown\CapsuleCRM\Connection');
    $this->opportunity = new Opportunity($this->connection);
    $this->message = m::mock('Guzzle\Http\Message\Response');
}

This is basically exactly the same code from the PartyTest class that we looked at last week.

Next we can write a simple test to ensure that the Opportunity class requires an instance of Connection:

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

    $o = new Opportunity("");
}

Next we can write a test to ensure that the find() method will correctly return an instance of Opportunity with the correct properties of the stub response:

/** @test */
public function find_opportunity_by_id()
{
    $response = file_get_contents(dirname(__FILE__).'/stubs/opportunity.json');
    $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
    $this->connection->shouldReceive('get')->andReturn($this->message);

    $opportunity = $this->opportunity->find(43);

    $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Opportunity', $opportunity);
    $this->assertEquals('43', $opportunity->id);
    $this->assertEquals('Consulting', $opportunity->name);
    $this->assertEquals('Scope and design web site shopping cart', $opportunity->description);
    $this->assertEquals('2', $opportunity->party_id);
    $this->assertEquals('GBP', $opportunity->currency);
    $this->assertEquals('500.00', $opportunity->value);
    $this->assertEquals('DAY', $opportunity->duration_basis);
    $this->assertEquals('10', $opportunity->duration);
    $this->assertEquals('2012-09-30T00:00:00Z', $opportunity->expected_close_date);
    $this->assertEquals('2', $opportunity->milestone_id);
    $this->assertEquals('Bid', $opportunity->milestone);
    $this->assertEquals('50', $opportunity->probability);
    $this->assertEquals('a.user', $opportunity->owner);
    $this->assertEquals('2011-09-30T00:00:00Z', $opportunity->created_on);
    $this->assertEquals('2011-09-30T00:00:00Z', $opportunity->updated_on);
}

And finally we can write a test to ensure that the all() method is returning a Collection of Opportunity instances:

/** @test */
public function find_all_opportunities()
{
    $response = file_get_contents(dirname(__FILE__).'/stubs/opportunities.json');
    $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
    $this->connection->shouldReceive('get')->andReturn($this->message);

    $collection = $this->opportunity->all();

    $this->assertInstanceOf('Illuminate\Support\Collection', $collection);
    $this->assertEquals(1, $collection->count());
    $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Opportunity', $collection[0]);
}

If you now run those tests you will see them fail because the find() and all() methods are returning null.

Normalising Opportunities

The reason the find() and all() methods are returning null is because we have only written the code to normalise models with subclasses.

In Normalizer we currently have this code:

/**
 * Normalize a single model
 *
 * @param array $attributes
 * @return PhilipBrown\CapsuleCRM\Model
 */
public function model(array $attributes)
{
    if ($this->hasSubclasses()) {
        return $this->normalizeSubclass($attributes);
    }
}

/**
 * Normalize a collection of models
 *
 * @param array $attributes
 * @return Illuminate\Support\Collection
 */
public function collection(array $attributes)
{
    if ($this->hasSubclasses()) {
        return $this->normalizeSubclassCollection($attributes);
    }
}

If the model does not have subclasses we can pass the array of $attributes to private methods that will deal with the response.

Update the code above to read as:

/**
 * Normalize a single model
 *
 * @param array $attributes
 * @return PhilipBrown\CapsuleCRM\Model
 */
public function model(array $attributes)
{
    if ($this->hasSubclasses()) {
        return $this->normalizeSubclass($attributes);
    }

    return $this->normalizeModel($attributes);
}

/**
 * Normalize a collection of models
 *
 * @param array $attributes
 * @return Illuminate\Support\Collection
 */
public function collection(array $attributes)
{
    if ($this->hasSubclasses()) {
        return $this->normalizeSubclassCollection($attributes);
    }

    return $this->normalizeCollection($attributes);
}

The first method we will tackle will be normalizeModel():

/**
 * Normalize a single model
 *
 * @param array $attributes
 * @return PhilipBrown\CapsuleCRM\Model
 */
private function normalizeModel(array $attributes)
{
    return $this->createNewModelInstance($this->model->base()->singular(), $attributes[(string) $this->root()]);
}

In this method we can simply retrieve the name of the model and then use it to pass the correct name and the array of entity properties from the $attributes array to the createNewModelInstance() from last week.

To normalise a collection we can use the following method:

/**
 * Normalize a collection
 *
 * @param array $attributes
 * @return Illuminate\Support\Collection
 */
private function normalizeCollection(array $attributes)
{
    $collection = new Collection;

    $type = (string) $this->collectionRoot();
    $root = (string) $this->root();

    foreach ($attributes[$type] as $entity) {
        $collection[] = $this->createNewModelInstance($root, $entity);
    }

    return $collection;
}

In this method we instantiate a new instance of Collection and we save the root and collection_root as local variables.

Next we can iterate over the $attributes array and pass each $entity into the createNewModelInstance() method. This will return a new Model instance that we can add to the $collection and then finally we can return the $collection from the method.

Now if you run the Opportunity tests you should see that they all pass.

The Kase and Task models

The next two models we will look at are Kase and Task. If you have a look at the documentation for these two resources you will see that both are pretty straight forward and so everything should work from the code we’ve already written.

The Kase model

Firstly we’ll look at the Kase model:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\Findable;

class Kase extends Model
{
    use Findable;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = [
        "id",
        "status",
        "name",
        "description",
        "party_id",
        "owner",
        "created_on",
        "updated_on",
    ];

    /**
     * The model's queryable options
     *
     * @var array
     */
    protected $queryableOptions = [
        "plural" => "kase",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

As you can see from the code above, this model is really straight forward. We need to set the plural option for the $queryableOptions, but other than that every should be good to go.

Next, create two new stub files called kase.json and kases.json and copy the following JSON:

{
    "kase": {
        "id": "43",
        "status": "OPEN",
        "name": "Consulting",
        "description": "Scope and design web site shopping cart",
        "partyId": "2",
        "owner": "a.user",
        "createdOn": "2011-04-16T13:59:58Z",
        "updatedOn": "2011-05-11T16:54:23Z"
    }
}
{
    "kases": {
        "kase": [{
            "id": "43",
            "createdOn": "2011-04-16T13:59:58Z",
            "description": "Scope and design web site shopping cart",
            "name": "Consulting",
            "status": "OPEN",
            "owner": "a.user",
            "updatedOn": "2011-05-11T16:54:23Z",
            "partyId": "2"
        }]
    }
}

Finally we can write the tests. These tests follow the same format as the OpportunityTest file and so there shouldn’t be much to explain:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Kase;

class KaseTest extends PHPUnit_Framework_TestCase {

/** @var PhilipBrown\CapsuleCRM\Connection */
private $connection;

/** @var PhilipBrown\CapsuleCRM\Kase */
private $model;

/** @var Guzzle\Http\Message\Response */
private $message;

public function setUp()
{
    $this->connection = m::mock('PhilipBrown\CapsuleCRM\Connection');
    $this->model = new Kase($this->connection);
    $this->message = m::mock('Guzzle\Http\Message\Response');
}

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

    $m = new Kase("");
}

/** @test */
public function find_case_by_id()
{
    $response = file_get_contents(dirname(__FILE__).'/stubs/kase.json');
    $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
    $this->connection->shouldReceive('get')->andReturn($this->message);

    $case = $this->model->find(43);

    $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Kase', $case);
    $this->assertEquals('43', $case->id);
    $this->assertEquals('OPEN', $case->status);
    $this->assertEquals('Consulting', $case->name);
    $this->assertEquals('Scope and design web site shopping cart', $case->description);
    $this->assertEquals('2', $case->party_id);
    $this->assertEquals('a.user', $case->owner);
    $this->assertEquals('2011-04-16T13:59:58Z', $case->created_on);
    $this->assertEquals('2011-05-11T16:54:23Z', $case->updated_on);
}

/** @test */
public function find_all_cases()
{
    $response = file_get_contents(dirname(__FILE__).'/stubs/kases.json');
    $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
    $this->connection->shouldReceive('get')->andReturn($this->message);

    $collection = $this->model->all();

    $this->assertInstanceOf('Illuminate\Support\Collection', $collection);
    $this->assertEquals(1, $collection->count());
    $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Kase', $collection[0]);
}

}

The Task Model

Again the Task model is even more straightforward than the Kase model:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\Findable;

class Task extends Model
{
    use Findable;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = [
        "id",
        "description",
        "detail",
        "category",
        "due_date",
        "owner",
        "party_id",
        "party_name",
        "status",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

In this example there is nothing to configure and so everything should just work straight out of the box.

Create another two stub files called task.json and tasks.json and copy the following JSON:

{
    "task": {
        "id": "100",
        "description": "Meet with customer",
        "detail": "Meeting at Coffee shop",
        "category": "Meeting",
        "dueDate": "2012-02-24T00:00:00Z",
        "owner": "a.user",
        "partyId": "1",
        "partyName": "Eric Jones",
        "status": "OPEN"
    }
}
{
    "tasks": {
        "task": [
            {
                "id": "100",
                "dueDate": "2012-02-24T00:00:00Z",
                "category": "Meeting",
                "partyName": "Eric Jones",
                "description": "Meet with customer",
                "owner": "a.user",
                "detail": "Meeting at Coffee shop",
                "partyId": "1"
            },
            {
                "id": "101",
                "dueDate": "2012-02-24T00:00:00Z",
                "category": "Meeting",
                "description": "Waste some time",
                "owner": "a.user",
                "detail": "Writing an open source gem instead of working",
                "opportunityId": "5",
                "opportunityName": "meeting"
            },
            {
                "id": "102",
                "dueDate": "2012-02-24T00:00:00Z",
                "category": "Meeting",
                "description": "Innovation time",
                "owner": "a.user",
                "detail": "Go and get drunk",
                "caseId": "3",
                "caseName": "I can't think of a good name"
            },
            {
                "id": "103",
                "dueDate": "2012-02-24T00:00:00Z",
                "category": "Meeting",
                "description": "An orphan task",
                "owner": "a.user",
                "detail": "Do something interesting"
            }
        ]
    }
}

Finally the tests file should once again be pretty much the same as the tests for Opportunity and Kase:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Task;

class TaskTest extends PHPUnit_Framework_TestCase {

    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\Task */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock('PhilipBrown\CapsuleCRM\Connection');
        $this->model = new Task($this->connection);
        $this->message = m::mock('Guzzle\Http\Message\Response');
    }

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

        $m = new Task("");
    }

    /** @test */
    public function find_task_by_id()
    {
        $response = file_get_contents(dirname(__FILE__).'/stubs/task.json');
        $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
        $this->connection->shouldReceive('get')->andReturn($this->message);

        $task = $this->model->find(100);

        $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Task', $task);
        $this->assertEquals('100', $task->id);
        $this->assertEquals('Meet with customer', $task->description);
        $this->assertEquals('Meeting at Coffee shop', $task->detail);
        $this->assertEquals('Meeting', $task->category);
        $this->assertEquals('2012-02-24T00:00:00Z', $task->due_date);
        $this->assertEquals('a.user', $task->owner);
        $this->assertEquals('1', $task->party_id);
        $this->assertEquals('Eric Jones', $task->party_name);
        $this->assertEquals('OPEN', $task->status);
    }

    /** @test */
    public function find_all_cases()
    {
        $response = file_get_contents(dirname(__FILE__).'/stubs/tasks.json');
        $this->message->shouldReceive('json')->andReturn(json_decode($response, true));
        $this->connection->shouldReceive('get')->andReturn($this->message);

        $collection = $this->model->all();

        $this->assertInstanceOf('Illuminate\Support\Collection', $collection);
        $this->assertEquals(4, $collection->count());
        $this->assertInstanceOf('PhilipBrown\CapsuleCRM\Task', $collection[0]);
    }
}

The User and Track models

If you have a look at the initial list of models to build and what abilities they should have you will see that both the User and the Track model should only be able to retrieve all resources, and they should not be able to find a single resource by id.

Fortunately we’ve already made this an easy problem to solve by splitting the all() and find() methods into two separate traits.

The User model

The first model we will look at will be for the User model:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\FindAll;
use PhilipBrown\CapsuleCRM\Querying\Configuration;

class User extends Model
{
    use FindAll;
    use Configuration;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = [
        "id",
        "username",
        "name",
        "currency",
        "timezone",
        "logged_in",
        "party_id",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

As you can see, instead of including the Findable trait, we can instead include the FindAll and Configuration traits. This will provide the all() method, but not the find() method.

We will only require a users.json file as there is only the all() method:

{
    "users": {
    "user": [
            {
                "id": "2",
                "username": "a.user",
                "name": "Alfred User",
                "currency": "GBP",
                "timezone": "Europe/London",
                "loggedIn": "true",
                "partyId": "100"
            },
            {
                "id": "3",
                "username": "j.joe",
                "name": "Jane Doe",
                "currency": "GBP",
                "timezone": "Europe/London",
                "partyId": "101"
            }
        ]
    }
}

And finally the UserTest file will only require to test the all() method. The User model should work straight away without any extra configuration:

use Mockery as m;
use PhilipBrown\CapsuleCRM\User;

class UserTest extends PHPUnit_Framework_TestCase
{
    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\User */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new User($this->connection);
        $this->message = m::mock("Guzzle\Http\Message\Response");
    }

    /** @test */
    public function find_all_users()
    {
        $response = file_get_contents(dirname(__FILE__) . "/stubs/users.json");
        $this->message
            ->shouldReceive("json")
            ->andReturn(json_decode($response, true));
        $this->connection->shouldReceive("get")->andReturn($this->message);

        $collection = $this->model->all();

        $this->assertInstanceOf("Illuminate\Support\Collection", $collection);
        $this->assertEquals(2, $collection->count());

        $this->assertInstanceOf("PhilipBrown\CapsuleCRM\User", $collection[0]);
        $this->assertEquals("2", $collection[0]->id);
        $this->assertEquals("a.user", $collection[0]->username);
        $this->assertEquals("Alfred User", $collection[0]->name);
        $this->assertEquals("GBP", $collection[0]->currency);
        $this->assertEquals("Europe/London", $collection[0]->timezone);
        $this->assertEquals("true", $collection[0]->logged_in);
        $this->assertEquals("100", $collection[0]->party_id);

        $this->assertInstanceOf("PhilipBrown\CapsuleCRM\User", $collection[1]);
        $this->assertEquals("3", $collection[1]->id);
        $this->assertEquals("j.joe", $collection[1]->username);
        $this->assertEquals("Jane Doe", $collection[1]->name);
        $this->assertEquals("GBP", $collection[1]->currency);
        $this->assertEquals("Europe/London", $collection[1]->timezone);
        $this->assertEquals("101", $collection[1]->party_id);
    }
}

The Track model

The Track model is very similar to the User model and should need no additional configuration:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\FindAll;
use PhilipBrown\CapsuleCRM\Querying\Configuration;

class Track extends Model
{
    use FindAll;
    use Configuration;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = ["id", "description", "capture_rule"];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

Again we only require a single stub file:

{
    "tracks": {
        "track": [
            {
                "id": "1",
                "description": "Sales follow up",
                "captureRule": "OPPORTUNITY"
            },
            {
                "id": "2",
                "description": "Customer service feedback",
                "captureRule": "CASEFILE"
            }
        ]
    }
}

And we only need to test for the all() method:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Track;

class TrackTest extends PHPUnit_Framework_TestCase
{
    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\Track */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new Track($this->connection);
        $this->message = m::mock("Guzzle\Http\Message\Response");
    }

    /** @test */
    public function find_all_countries()
    {
        $response = file_get_contents(dirname(__FILE__) . "/stubs/tracks.json");
        $this->message
            ->shouldReceive("json")
            ->andReturn(json_decode($response, true));
        $this->connection->shouldReceive("get")->andReturn($this->message);

        $collection = $this->model->all();

        $this->assertInstanceOf("Illuminate\Support\Collection", $collection);
        $this->assertEquals(2, $collection->count());
    }
}

The History Model

The History model only requires the find() method and so we can just include the FindOne and Configuration traits:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\FindOne;
use PhilipBrown\CapsuleCRM\Querying\Configuration;

class History extends Model
{
    use FindOne;
    use Configuration;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = [
        "id",
        "type",
        "entry_date",
        "creator",
        "creator_name",
        "subject",
        "note",
        "attachments",
    ];

    /**
     * The model's serializble config
     *
     * @var array
     */
    protected $serializableConfig = [
        "root" => "historyItem",
        "collection_root" => "history",
    ];

    /**
     * The model's queryable options
     *
     * @var array
     */
    protected $queryableOptions = [
        "plural" => "history",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

You will also notice that for some bizarre reason the root of the response should be historyItem.

Create a new history.json file and copy the following JSON:

{
    "historyItem": {
        "id": "100",
        "type": "Note",
        "entryDate": "2009-09-11T16:07:49Z",
        "creator": "a.user",
        "creatorName": "A User",
        "subject": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla mollis ullam...",
        "note": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla mollis ullamcorper vehicula.",
        "attachments": {
            "attachment": {
                "id": "20",
                "filename": "latin.doc",
                "contentType": "application/msword"
            }
        }
    }
}

And finally the HistoryTest file will only require a test for the find() method:

use Mockery as m;
use PhilipBrown\CapsuleCRM\History;

class HistoryTest extends PHPUnit_Framework_TestCase
{
    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\History */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new History($this->connection);
        $this->message = m::mock("Guzzle\Http\Message\Response");
    }

    /** @test */
    public function find_history_by_id()
    {
        $response = file_get_contents(
            dirname(__FILE__) . "/stubs/history.json"
        );
        $this->message
            ->shouldReceive("json")
            ->andReturn(json_decode($response, true));
        $this->connection->shouldReceive("get")->andReturn($this->message);

        $item = $this->model->find(100);

        $this->assertInstanceOf("PhilipBrown\CapsuleCRM\History", $item);
        $this->assertEquals("100", $item->id);
        $this->assertEquals("Note", $item->type);
        $this->assertEquals("2009-09-11T16:07:49Z", $item->entry_date);
        $this->assertEquals("a.user", $item->creator);
        $this->assertEquals("A User", $item->creator_name);
        $this->assertEquals(
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla mollis ullam...",
            $item->subject
        );
        $this->assertEquals(
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla mollis ullamcorper vehicula.",
            $item->note
        );
        $this->assertTrue(is_array($item->attachments));
    }
}

The Country and Currency models

Finally we have the Country and Currency models. If you have a look at the documentation for the Country and Currency endpoints you will see that the response does not really follow the same format as the responses for the other endpoints. Instead of returning an associative array of keys and values, these two endpoints return an indexed array of values.

This is a problem because we need to be able to assign those values to properties on the model.

To solve this problem we need to add an attribute_to_assign property to the $serializableConfig.

Here is the Country model:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\FindAll;
use PhilipBrown\CapsuleCRM\Querying\Configuration;

class Country extends Model
{
    use FindAll;
    use Configuration;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = ["name"];

    /**
     * The model's serializble config
     *
     * @var array
     */
    protected $serializableConfig = [
        "attribute_to_assign" => "name",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

The countries.json stub file should be just a list of available countries:

{
    "countries": {
        "country": [
            "Brazil",
            "France"
        ]
    }
}

And the test file only needs to test for the all() method:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Country;

class CountryTest extends PHPUnit_Framework_TestCase
{
    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\Country */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new Country($this->connection);
        $this->message = m::mock("Guzzle\Http\Message\Response");
    }

    /** @test */
    public function find_all_countries()
    {
        $response = file_get_contents(
            dirname(__FILE__) . "/stubs/countries.json"
        );
        $this->message
            ->shouldReceive("json")
            ->andReturn(json_decode($response, true));
        $this->connection->shouldReceive("get")->andReturn($this->message);

        $collection = $this->model->all();

        $this->assertInstanceOf("Illuminate\Support\Collection", $collection);
        $this->assertEquals(2, $collection->count());
    }
}

The Currency model is very similar to the Country model:

<?php namespace PhilipBrown\CapsuleCRM;

use PhilipBrown\CapsuleCRM\Querying\FindAll;
use PhilipBrown\CapsuleCRM\Querying\Configuration;

class Currency extends Model
{
    use FindAll;
    use Configuration;
    use Serializable;

    /**
     * The model's fillable attributes
     *
     * @var array
     */
    protected $fillable = ["code"];

    /**
     * The model's serializble config
     *
     * @var array
     */
    protected $serializableConfig = [
        "attribute_to_assign" => "code",
    ];

    /**
     * Create a new instance of the model
     *
     * @param PhilipBrown\CapsuleCRM\Connection $connection
     * @param array $attributes;
     * @return void
     */
    public function __construct(Connection $connection, array $attributes = [])
    {
        parent::__construct($connection);

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

Again the currencies.json file is a simple array of currencies:

{
    "currencies": {
        "currency": [
            "AUD",
            "CAD",
            "EUR"
        ]
    }
}

And once again, the test file only needs to test for the all() method:

use Mockery as m;
use PhilipBrown\CapsuleCRM\Currency;

class CurrencyTest extends PHPUnit_Framework_TestCase
{
    /** @var PhilipBrown\CapsuleCRM\Connection */
    private $connection;

    /** @var PhilipBrown\CapsuleCRM\Currency */
    private $model;

    /** @var Guzzle\Http\Message\Response */
    private $message;

    public function setUp()
    {
        $this->connection = m::mock("PhilipBrown\CapsuleCRM\Connection");
        $this->model = new Currency($this->connection);
        $this->message = m::mock("Guzzle\Http\Message\Response");
    }

    /** @test */
    public function find_all_countries()
    {
        $response = file_get_contents(
            dirname(__FILE__) . "/stubs/currencies.json"
        );
        $this->message
            ->shouldReceive("json")
            ->andReturn(json_decode($response, true));
        $this->connection->shouldReceive("get")->andReturn($this->message);

        $collection = $this->model->all();

        $this->assertInstanceOf("Illuminate\Support\Collection", $collection);
        $this->assertEquals(3, $collection->count());
    }
}

Conclusion

Phew! That was a lot of code to look at. If you managed to get this far, give yourself a pat on the back.

We’ve now finished implementing the code to query the API. We can now take the raw JSON responses, and transform them into the appropriate model objects at runtime.

Whilst a lot of the code in this tutorial was repeated, I think the real lesson should be how you should treat testing an API.

You never want to actually make HTTP requests during your tests. Instead you should be taking the stub JSON responses from the API documentation, and use those to ensure your code can handle it correctly.

Next week we can start looking at implementing the code to add, update and delete resources from the API.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.