cult3

How to create an Active Record style PHP SDK Part 15

Oct 15, 2014

Table of contents:

  1. What are embedded objects?
  2. How are we going to build this?
  3. The model objects
  4. The Contacts object
  5. The Contactable Trait
  6. Adding the contacts to the models
  7. Adding the Contacts collection to the tests
  8. Conclusion

Last week we looked at building out the functionality to serialise model objects into JSON that can be sent to the CapsuleCRM API.

We are now at the stage where we can call the toJson() method on a model object and have that object serialise itself into JSON that satisfies the requirements of the API.

However, one thing we missed off last week was dealing with embedded objects.

In this week’s tutorial we’re going to look at implementing embedded objects for the Person and Organisation classes.

What are embedded objects?

If we have a look at the API documentation for Parties, we can see that the sample JSON includes a collection of contacts.

The contacts collection is comprised of addresses, websites, emails phone number objects.

However, the contacts collection does not seem to be a relationship in the traditional sense of the Active Record pattern.

Instead, the contacts collection is an embedded object within the model object. From the outside in, it appears that the contacts object is a Value Object.

How are we going to build this?

Before I jump into the code, first I’ll explain how this is going to work.

Each of the items of the contacts collection will be model instances that inherit from the abstract Model class.

The contacts collection itself will be a PHP object that has it’s own unique implementation of the toJson() method for iterating through the collection and serialising each item to JSON.

The Person and the Organisation models will implement a Contactable trait that will make addContacts(Contacts $contacts) and a contacts() method available.

The Person and Organisation models will also have an another serialisable config option so the serialisation process knows it has to include the contacts collection when the object is getting serialised.

If any of that seems a bit abstract or doesn’t make sense, keep reading! I’m sure it will all fall in to place once you see the implementation.

The model objects

The first thing to do is to create the new model objects. These objects are pretty dumb, so we don’t need to do anything fancy.

The Address object looks like this:

<?php namespace PhilipBrown\CapsuleCRM;

class Address extends Model
{
    use Serializable;

    /**
     * @var array
     */
    protected $attributes;

    /**
     * @var array
     */
    protected $fillable = [
        "id",
        "type",
        "street",
        "city",
        "state",
        "zip",
        "country",
    ];

    /**
     * The serializble config
     *
     * @var array
     */
    protected $serializableConfig = [
        "include_root" => false,
        "exclude_id" => false,
    ];

    /**
     * Create a new Address
     *
     * @param array $attributes
     * @return void
     */
    public function __construct(array $attributes)
    {
        $this->fill($attributes);
    }
}

As you can see, all we have to do is specify the fillable array and define a couple of serializableConfig options.

The tests for this class are also really simple:

use PhilipBrown\CapsuleCRM\Address;

class AddressTest extends PHPUnit_Framework_TestCase
{
    /** @var Address */
    private $address;

    public function setUp()
    {
        $this->address = new Address([
            "type" => "Office",
            "street" => "101 Blah Blah Lane",
            "city" => "London",
            "zip" => "E20 123",
            "country" => "England",
        ]);
    }

    /** @test */
    public function should_create_new_address()
    {
        $this->assertEquals("Office", $this->address->type);
        $this->assertEquals("101 Blah Blah Lane", $this->address->street);
        $this->assertEquals("London", $this->address->city);
        $this->assertEquals("E20 123", $this->address->zip);
        $this->assertEquals("England", $this->address->country);
    }

    /** @test */
    public function should_serialize()
    {
        $this->assertTrue(is_object(json_decode($this->address->toJson())));
    }
}

The first step simply checks to see if the properties are getting set correctly and the second object is checking to ensure the model can be serialised correctly.

I won’t cover the Website, Phone and Email objects because there is a lot of repeated code. If you want to check out how they work, take a look at the GitHub repository.

The Contacts object

The Contacts object is simply going to be a PHP object that can hold instances of the model objects we’ve created above. The Contacts object also needs to be serialisable so we can convert the collection to JSON when the parent object is being serialised.

So the first thing to do is create the class:

<?php namespace PhilipBrown\CapsuleCRM;

class Contacts
{
    /**
     * @var array
     */
    protected $addresses;

    /**
     * @var array
     */
    protected $emails;

    /**
     * @var array
     */
    protected $phones;

    /**
     * @var array
     */
    protected $websites;

    /**
     * Create a new Contacts collection
     *
     * @param array $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {
        $this->addresses = isset($attributes["addresses"])
            ? $attributes["addresses"]
            : [];
        $this->emails = isset($attributes["emails"])
            ? $attributes["emails"]
            : [];
        $this->phones = isset($attributes["phones"])
            ? $attributes["phones"]
            : [];
        $this->websites = isset($attributes["websites"])
            ? $attributes["websites"]
            : [];
    }
}

As you can see, the class accepts an optional $attributes array. This array will hold optional instances of each type of the contact models.

We can also provide a couple of helper methods for adding arrays of model instances:

/**
 * Set an array of addresses
 *
 * @param array $addresses
 * @return void
 */
public function addresses(array $addresses)
{
    $this->addresses = $addresses;
}

/**
 * Set an array of emails
 *
 * @param array $emails
 * @return void
 */
public function emails(array $emails)
{
    $this->emails = $emails;
}

/**
 * Set an array of phones
 *
 * @param array $phones
 * @return void
 */
public function phones(array $phones)
{
    $this->phones = $phones;
}

/**
 * Set an array of websites
 *
 * @param array $websites
 * @return void
 */
public function websites(array $websites)
{
    $this->websites = $websites;
}

I’ll also implement the magic __get() helper method so the private arrays of model instances can read as if they were public properties.

Next we need to implement the toJson() method. This implementation does not need to go through the process of the Serializer object and so we can just deal with the serialisation internally to this class. The outside world will never know the difference:

/**
 * Convert the Contacts collection to JSON
 *
 * @return string
 */
public function toJson() {}

To convert this collection to JSON we need a way to iterate through each of the arrays of model instances and call the toJson() method on each:

/**
 * Map each item of the collection
 * and serialise to JSON
 *
 * @param array $attributes
 * @return array
 */
private function map(array $attributes)
{
    if (count($attributes) == 1) {
        return json_decode($attributes[0]->toJson());
    }

    return array_map(function($obj) { return json_decode($obj->toJson()); }, $attributes);
}

In this method we first check to see if the $attributes array contains only a single model. If it does we can simply call the toJson() and return it.

However if there is more than one model in the array, we can use the array_map() function to iterate over them.

Annoyingly, PHP doesn’t like it if you JSON encode the same thing twice. This means we need to use the json_decode() function to return the JSON as an object. This object will then get re-encoded in the toJson() method.

Finally we can implement the toJson() method:

/**
 * Convert the Contacts collection to JSON
 *
 * @return string
 */
public function toJson()
{
    $body = [
        'address' => $this->map($this->addresses),
        'email' => $this->map($this->emails),
        'phone' => $this->map($this->phones),
        'website' => $this->map($this->websites),
    ];

    $body = array_filter($body, function ($item) {
        return count($item);
    });

    return json_encode($body);
}

In this method we create a new $body variable and invoke the $this->map() method for each of the types of contact model.

We then use the array_filter() to remove any empty arrays and then we can encode the array to JSON and return it.

The tests for the Contacts class are pretty straight forward:

use PhilipBrown\CapsuleCRM\Email;
use PhilipBrown\CapsuleCRM\Phone;
use PhilipBrown\CapsuleCRM\Website;
use PhilipBrown\CapsuleCRM\Address;
use PhilipBrown\CapsuleCRM\Contacts;

class ContactsTest extends PHPUnit_Framework_TestCase
{
    /** @var Address */
    private $address;

    /** @var Email */
    private $email;

    /** @var Phone */
    private $phone;

    /** @var Website */
    private $website;

    public function setUp()
    {
        $this->address = new Address(["street" => "101 Blah Blah Lane"]);
        $this->email = new Email(["email_address" => "name@domain.com"]);
        $this->phone = new Phone(["phone_number" => "0191 123 456"]);
        $this->website = new Website([
            "web_service" => "TWITTER",
            "web_address" => "philipbrown",
        ]);
    }

    /** @test */
    public function should_create_contact()
    {
        $contacts = new Contacts([
            "addresses" => [$this->address],
            "emails" => [$this->email],
            "phones" => [$this->phone],
            "websites" => [$this->website],
        ]);

        $this->assertInstanceOf(
            "PhilipBrown\CapsuleCRM\Address",
            $contacts->addresses[0]
        );
        $this->assertInstanceOf(
            "PhilipBrown\CapsuleCRM\Email",
            $contacts->emails[0]
        );
        $this->assertInstanceOf(
            "PhilipBrown\CapsuleCRM\Phone",
            $contacts->phones[0]
        );
        $this->assertInstanceOf(
            "PhilipBrown\CapsuleCRM\Website",
            $contacts->websites[0]
        );
    }

    /** @test */
    public function should_serialise()
    {
        $contacts = new Contacts(["emails" => [$this->email]]);

        $this->assertTrue(is_object(json_decode($contacts->toJson())));
    }
}

In the first test we check to see if the Contacts class is getting created correctly and in the second test we check to see if the object is getting encoded to JSON correctly.

The Contactable Trait

Both of the Person and the Organisation models need two methods for adding and retrieving the contacts collection.

This is the perfect job for a trait! (What are PHP Traits?)

<?php namespace PhilipBrown\CapsuleCRM;

trait Contactable
{
    /**
     * @var Contacts
     */
    private $contacts;

    /**
     * Add Contacts
     *
     * @param Contacts $contact
     * @return void
     */
    public function addContacts(Contacts $contacts)
    {
        $this->contacts = $contacts;
    }

    /**
     * Get Contacts
     *
     * @return Contacts
     */
    public function contacts()
    {
        return $this->contacts;
    }
}

Adding the contacts to the models

Due to the fact that this isn’t strictly speaking a relationship between models, we shouldn’t treat it as such. Fortunately because we have the $serializableConfig options array we can notify the Serializer object to take a certain action for these two models.

Add a new key, value pair to the $serializableConfig like this:

'additional_methods' => ['contacts']

This is saying, when this object is getting serialised, call the contacts() method. This will return the contacts collection which can be serialised as part of the model.

Finally we can update the attributes() method of the Serializer class to check for additional_methods:

/**
 * Get and format the model attributes
 *
 * @return array
 */
private function attributes()
{
    $attributes = [];

    foreach ($this->options['additional_methods'] as $method) {
        $attributes = array_merge($attributes, [$method => json_decode($this->model->$method()->toJson())]);
    }

    $attributes = array_merge($attributes, $this->model->attributes());

    return $attributes;
}

By default the additional_methods option will be an empty array. However for the Person and Organisation models, this foreach loop will iterate through the additional methods, call them, and return the JSON.

This allows us to add more additional methods in the future without having to rewrite this code.

Adding the Contacts collection to the tests

Finally we can add the Contacts collection to the Person and Organisation model tests to satisfy the stub requests:

/** @test */
public function should_serialize_model()
{
    $person = new Person($this->connection, [
        'title' => 'Mr',
        'first_name' => 'Eric',
        'last_name' => 'Schmidt',
        'job_title' => 'Chairman',
        'organisation_name' => 'Google Inc',
        'about' => 'A comment here'
    ]);

    $person->addContacts(
        new Contacts([
        'addresses' => [new Address([
            'type' => 'Office',
            'street' => '1600 Amphitheatre Parkway',
            'city' => 'Mountain View',
            'state' => 'CA',
            'zip' => '94043',
            'country' => 'United States'
        ])],
        'emails' => [new Email([
            'type' => 'Home',
            'email_address' => 'e.schmidt@google.com'
        ])],
        'phones' => [new Phone([
            'type' => 'Mobile',
            'phone_number' => '+1 888 555555'
        ])],
        'websites' => [new Website([
            'type' => 'work',
            'web_service' => 'URL',
            'web_address' => 'www.google.com'
        ])]
        ])
    );

    $stub = json_decode(file_get_contents(dirname(__FILE__).'/stubs/post/person.json'), true);

    $this->assertEquals(json_encode($stub), $person->toJson());
}

As you can see from this test we simply need to instantiate the Contacts collection with instances of the correct contact models.

We can now also remove the unset() function from last week, and then assert that the model object can be serialised into what the stub request is expecting.

Conclusion

Phew, That was a lot to cover! Well done for getting this far!

Addresses and contact details are very often treated as Value Objects within an application. You should avoid treating an object as an Entity if you can help it because introducing state and object life cycles can over-complicate the simplest of jobs.

Embedding an Value Object into an Entity is a really good way of working with a data as part of an application. The Value Object can dictate it’s own rules and the Entity does not have to deal with things it should not be concerned about.

Today we’ve seen how we can implement embedded object into an model object so that we can send the entity to the API. I think modelling the contacts of the Person and Organisation as a Value Object is a great way of doing things. It was not that difficult to implement, and we’ve left the door open to further embedded Value Objects without tying ourselves to a single implementation.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.