Sep 24, 2014
Table of contents:
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.
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
Kase
History
Task
User
Country
Currency
Track
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 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.
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"
}
]
}
}
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.
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
.
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 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.
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]);
}
}
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]);
}
}
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 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 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 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));
}
}
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());
}
}
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.