Jul 30, 2014
Table of contents:
Last week we started looking at building out the abstract model that will form the foundation of this Active Record style PHP SDK.
The models are one of the most important components of the Active Record pattern due to the fact that they hold so much responsibility of the entity you are working with, as well as the means of storing those entities to the persistent storage.
In this week’s tutorial we’re going to continue looking at building the foundational layer of the abstract Model
class. Today we’ll be looking at setting and getting the model properties as well as inspecting the model instance using Reflection (What is Reflection in PHP?).
Before I jump back into the code, first let’s have a little recap from last week. If you haven’t already read last week’s post, you should go do that now.
The Active Record pattern works by representing each resource as a Model
object. This object has the ability to find, create, update and delete entities from the storage. Each object basically maps to a single row in the database. The object also allows you to manipulate the properties of the object, as well as retrieve related entities through defined relationships.
Typical usage of the Active Record pattern would look something like this:
// Create a new user
$user = User::create(["name" => "Philip Brown"]);
// Set a property and save
$user->email = "name@domain.com";
$user->save();
// Find a user
$user = User::find(1);
// Delete a user
$user->delete();
In today’s tutorial we’re going to be looking at getting and setting the properties of the model objects as well as inspecting the objects at runtime using PHP’s ReflectionClass
API.
The first thing we’ll look at is how we get and set properties on the model objects.
There are a couple of different ways of implementing getting and setting properties on a PHP object.
From the code example above, you might be thinking that we could just implement getting and setting properties using public
class properties:
$user = new StdClass();
$user->name = "Philip Brown";
echo $user->name; // Philip Brown
However this would be a pretty bad way of implementing this functionality. Generally speaking you don’t want to have an object’s properties set as public
.
In this case I don’t want the properties of the object to be set as public
because I need to be able to control what properties are set on the object in order to correctly communicate with the API.
The first thing I’m going to do is to write a couple of tests to ensure that the functionality of the code we’re going to write is working correctly.
Open up the ModelTest.php
file from last week and update setUp()
like this:
public function setUp()
{
$this->model = new ModelStub(new Connection(", "), ['name' => 'Philip Brown']);
}
I want to be able to inject the properties of an entity into the model when it is created so we may as well do this in the setUp()
method so we don’t have to repeat this code.
Next I will need to assert that the name
is being set correctly:
public function testSettingAnArrayOfAttributes()
{
$this->assertEquals('Philip Brown', $this->model->name);
}
I also want to test to ensure that setting a property is working correctly too:
public function testSettingAProperty()
{
$this->model->email = 'name@domain.com';
$this->assertEquals('name@domain.com', $this->model->email);
}
Give phpunit
a blast and watch those tests fail.
Now we’ll write the code to implement this functionality!
As I mentioned above, setting the properties of the entity as just public
properties of the object would be a really bad idea.
However setting the properties as protected
object properties would also be a bad idea. One of the defining characteristics of the Active Record pattern is that the model will determine the properties of the object by reading the schema of the database.
Whilst we are using the Active Record pattern for this package, I don’t think trying to replicate this functionality when working with a third party API would be a good idea. All of the set up and infrastructure wouldn’t be worth it. However we can mimic this functionality fairly easily.
I’ve mentioned a few times in this series that a really good example of an Active Record implementation in PHP is Laravel’s Eloquent.
Eloquent allows you to specify which properties of the model are fillable
. This prevents the problem of Mass Assignment.
We can use this same functionality in our models to define which properties of the model should be allowed to be set.
Open up the Model.php
to make the following changes.
Firstly we’re going to need to add two protected
properties to the Model
:
/**
* The model's attributes
*
* @var array
*/
protected $attributes = [];
/**
* The model's fillable attributes
*
* @var array
*/
protected $fillable = [];
The $attributes
array will how the properties of the current entity such as 'Philip Brown'
.
The $fillable
array will hold the attributes that we want to define as being “fillable”. This will be an empty array in this abstract model, but every child model will be required to define it’s properties.
Next we’ll define a method for setting the properties on the $attributes
array:
/**
* Set attribute on object
*
* @param string $key
* @param string $mixed
* @return void
*/
protected function setAttribute($key, $value)
{
$this->attributes[$key] = $value;
}
Next we can define a method to check to see if a property is “fillable”:
/**
* Determine if the given attribute may be mass assigned
*
* @param string $key
* @return bool
*/
protected function isFillable($key)
{
if (in_array($key, $this->fillable)) return true;
}
Next we can define a method for stripping out only the “fillable” attributes of an array:
/**
* Get the fillable attributes of a given array
*
* @param array $attributes
* @return array
*/
protected function fillableFromArray(array $attributes)
{
if (count($this->fillable) > 0) {
return array_intersect_key($attributes, array_flip($this->fillable));
}
return $attributes;
}
Next we can define the fill()
method that will accept an array of properties and then set them if they are “fillable”:
/**
* Fill the entity with an array of attributes.
*
* @param array $attributes
*/
protected function fill(array $attributes)
{
foreach ($this->fillableFromArray($attributes) as $key => $value) {
if ($this->isFillable($key)) {
$this->setAttribute($key, $value);
}
}
}
And finally we can set the __get()
and __set()
magic methods (What are PHP Magic Methods?) to give the illusion of getting and setting public object properties:
/**
* Dynamically get an attribute
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
throw new Exception("{$key} is not a valid property");
}
/**
* Dynamically set an attribute.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function __set($key, $value)
{
if ($this->isFillable($key)) {
return $this->setAttribute($key, $value);
}
throw new Exception("{$key} is not a valid property");
}
You’ll notice that I’m throwing an Exception
in the two magic methods. Personally I find it annoying if I try to get or set a property but it just fails silently by returning null
.
All credit has to go to Laravel’s Eloquent for these very simple, yet very powerful methods that allow this functionality.
Now that this functionality is set up we need to update the ModelStub
class:
class ModelStub extends PhilipBrown\CapsuleCRM\Model
{
protected $fillable = ["name", "email"];
public function __construct(Connection $connection, $attributes = [])
{
parent::__construct($connection);
$this->fill($attributes);
}
}
Now if you run the test from earlier they should be all green!
The next thing we are going to implement is the ability to inspect the current model at runtime. This is important because we need to map the current model instance to the correct API endpoint.
For example, we need to map an instance of User
to the users
endpoint.
Now technically we could do this by just requiring that each model instance has a property like $endpoint
set. This would solve this problem, but we’re likely going to need to get this kind of meta information about the object in a couple of different ways.
Using the PHP’s ReflectionClass
also makes for a much more interesting tutorial!
The first thing I’m going to do is to write out a test for how I want this functionality to work:
public function testGetPluralEntityName()
{
$this->assertEquals('modelstubs', $this->model->base()->lowercase()->plural());
}
In the test above you’ll notice that I want to define a method called base()
that will allow me to chain string modifiers together to get the name of the class, make it lowercase and then make it plural. This will allow us to have a model called User
and then be able to automatically get the endpoint of users
.
Run the test and watch it fail.
Now let’s implement the code to make it work.
The first thing I need to do is to define a method called base()
on the Model
class. This method will return a new instance of a Base
class (that we’ll implement in a minute):
/**
* Return the base meta class
*
* @return PhilipBrown\CapsuleCRM\Meta\Base
*/
public function base()
{
return new Base($this);
}
You’ll notice this class is injected with an instance of the current object $this
. I’ve also created a new namespace Meta
to hold this Base
class. This is simply for a little bit of code organisation.
Next we’ll define the Base
class:
<?php namespace PhilipBrown\CapsuleCRM\Meta;
use ReflectionClass;
use PhilipBrown\CapsuleCRM\Model;
class Base
{
/**
* The ReflectionClass instance
*
* @var ReflectionClass
*/
protected $reflection;
/**
* The class to inspect
*
* @param PhilipBrown\CapsuleCRM\model $model
* @return void
*/
public function __construct(Model $model)
{
$this->reflection = new ReflectionClass($model);
}
}
This Base
class is really just a wrapper for the ReflectionClass
API. You’ll notice that in the __construct()
method I’m just creating a new instance of the ReflectionClass
.
The Base
class is acting as a wrapper to the ReflectionClass
so we can add our own methods. We’ve already seen two of those required methods in the test lowercase()
and plural()
. I’ll also add uppercase()
and singular()
as two more methods we need to implement.
Each of these four methods are responsible for morphing a string. This is something that we can encapsulate in a dedicated class instead of bloating the Base
class with extra functionality and responsibility.
Create a new file called Name.php
under the Meta
namespace with the following code:
<?php namespace PhilipBrown\CapsuleCRM\Meta;
class Name
{
/**
* The name
*
* @var string
*/
protected $name;
/**
* Create a new Name instance
*
* @param string $name
* @return void
*/
public function __construct($name)
{
$this->name = $name;
}
/**
* Convert the name to lowercase
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function lowercase()
{
}
/**
* Convert the name to uppercase
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function uppercase()
{
}
/**
* Convert the name to plural
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function plural()
{
}
/**
* Convert the name to singular
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function singular()
{
}
/**
* Return the name as a string
*
* @return string
*/
public function __toString()
{
return $this->name;
}
}
This class accepts a string``$name
and sets it as a property of the class.
I’ve defined the four methods we’re going to need already. You’ll notice that each method will return a new instance of the Name
class. This means we can chain the methods together to modify the name of the class in multiple ways.
Finally I’ve also defined the __toString()
method so we can type hint the class as a string to return the $name
. You can think of this class as if we were opening up the PHP string
type and adding a couple of extra methods (if strings in PHP had methods).
Next we can implement each of the methods.
The first method we’ll look at is lowercase()
:
/**
* Convert the name to lowercase
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function lowercase()
{
return new Name(strtolower($this->name));
}
In this method we can simply use PHP’s inbuilt strtolower()
function.
Similarly, to make strings uppercase we can just use PHP’s inbuilt function:
/**
* Convert the name to uppercase
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function uppercase()
{
return new Name(strtoupper($this->name));
}
Getting the pluralised or singular form of a word is a bit more tricky, especially because of all the weird rules of the English language.
Fortunately this is not a new problem so we can stand on the shoulders of giants. Laravel has already solved this problem for us and so we can simply using their implementation in our code.
Add the following to your composer.json
file and run composer update
:
"illuminate/support": "~4.1"
Next import the namespace into the Name
class:
use Illuminate\Support\Pluralizer;
And finally we can define the plural()
and singular
methods:
/**
* Convert the name to plural
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function plural()
{
return new Name(Pluralizer::plural($this->name));
}
/**
* Convert the name to singular
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
public function singular()
{
return new Name(Pluralizer::singular($this->name));
}
You’ll notice that I’m not injecting the Pluralizer
object into the class and I’m using static
methods gasp.
This is actually a rare case when you don’t need to inject the dependency and you can use static
methods.
Firstly we’re never going to need to mock the Pluralizer
so we don’t need to inject it. The ability to pluralise strings is just an enhancement to strings and should really be a PHP default function anyway.
We can also freely using static
methods because these method calls have no global state. A good general rule for when to use static
methods is if running the method again and again with the same input will always return the same output.
Finally we can add methods to the Base
class to pass the name of the current model to the Name
class for modification:
<?php namespace PhilipBrown\CapsuleCRM\Meta;
use ReflectionClass;
use PhilipBrown\CapsuleCRM\Model;
class Base
{
/**
* The ReflectionClass instance
*
* @var ReflectionClass
*/
protected $reflection;
/**
* The class to inspect
*
* @param PhilipBrown\CapsuleCRM\model $model
* @return void
*/
public function __construct(Model $model)
{
$this->reflection = new ReflectionClass($model);
}
/**
* Convert the name to lowercase
*
* @return PhilipBrown\CapsuleCRM\Meta\ClassName
*/
public function lowercase()
{
return $this->getNameInstance()->lowercase();
}
/**
* Convert the name to uppercase
*
* @return PhilipBrown\CapsuleCRM\Meta\ClassName
*/
public function uppercase()
{
return $this->getNameInstance()->uppercase();
}
/**
* Convert the name to plural
*
* @return PhilipBrown\CapsuleCRM\Meta\ClassName
*/
public function plural()
{
return $this->getNameInstance()->plural();
}
/**
* Convert the name to singular
*
* @return PhilipBrown\CapsuleCRM\Meta\ClassName
*/
public function singular()
{
return $this->getNameInstance()->singular();
}
/**
* Create new Name instance
*
* @return PhilipBrown\CapsuleCRM\Meta\Name
*/
protected function getNameInstance()
{
return new Name($this->reflection->getShortName());
}
}
With all the code implemented we can now run the tests again and watch them pass!
We’ve covered quite a bit in this tutorial and so we’re well on the way to creating an solid foundation for the model objects of this package.
Firstly we creating the functionality to set properties on model instances that enable the Active Record style of public properties. We’ve abstracted this functionality into the abstract model class so all of the child classes simply need to define what properties may be set.
Secondly we create a base class so we can use PHP’s powerful Reflection API to inspect the current object at run time. This means we can determine which endpoint to hit just by looking at the current object we’re working with. We also looked at how we can encapsulate logic into smaller, dedicated classes, rather than creating monolithic classes that try to do far too much.
We’ve looked at quite a bit today, so if you want to take a look through the source code remember it’s all on GitHub.