Horizontal Code Reuse Through Traits Posted on February 22nd, 2014
As of version 5.4, PHP has exposed horizontal code reuse through Traits. Horizontal code reuse allows developers to bypass class hierarchies and reuse bits and pieces of code in other classes. Prior to traits, developers would often put functionality in parent class as shown below.
<?php class BaseModel
{
protected $fillable = array();
public function hydrate(array $rawData)
{
foreach ($this->fillable as $attribute) {
$this->$attribute = $rawData[$attribute];
}
}
// Other Model specific code
}
class User extends BaseModel
{
protected $fillable = ['id', 'username', 'password'];
protected $id;
protected $username;
protected $password;
}
$user = new User;
$user->hydrate(array('id' => 1, 'username' => 'baileylo', 'password' => 'password'));
There doesn't appear to be anything wrong with this code and it doesn't look all that different from the trait implementation. But the benefit of traits is it allows you to avoid this class hierarchy. At some point you will find yourself writing a class that would benefit from the hydrate function, but it wouldn't require the other code found in BaseModel. Before Traits you only had two options, copy and paste code or update your class hierarchy to look like this
<?php
class Hydrate
{
// Implements hydrate method
}
class BaseModel extends Hydrate {}
class User extends BaseModel {}
class OtherObject extends Hydrate{}
While this is some what maintainable with one hydrate method, it will become unruly in the future. If you were to implement this in traits it could look like this:
<?php trait HydrateTrait
{
protected $fillable = [];
public function hydrate(array $raw_data)
{
foreach ($this->fillable as $attribute) {
if (array_key_exists($attribute, $raw_data)) {
$func = 'set' . lcfirst($attribute);
if (method_exists($this, $func)) {
$this->$func($raw_data[$attribute]);
} else {
$this->$attribute = $raw_data[$attribute];
}
}
}
}
}
trait DateColumnTrait
{
protected $createdOn;
protected $updatedOn;
public function setCreatedOn($mysqlDateString)
{
$this->createdOn = new DateTime($mysqlDateString);
}
public function setUpdatedOn($mysqlDateString)
{
$this->updatedOn = new DateTime($mysqlDateString);
}
}
class User
{
use HydrateTrait;
use DateColumnTrait;
protected $id;
protected $slug;
protected $name;
public function __construct()
{
$this->fillable = ['id', 'slug', 'name', 'createdOn', 'updatedOn'];
}
}
class OtherObject
{
use HydrateTrait;
protected $fillable = ['name', 'variable', 'parameter'];
protected $name;
protected $variable;
protected $parameter;
}
From this implementation you can see the benefits of traits. I no longer have the larger than necessary class hierarchy. Also the names of traits themselves allude to the functionality provided. In my original example BaseModel
does not inform the user of any specific functionality; instead the name implies that all models should extend this BaseModel
. With HydrateTrait the reader has a good idea of what the trait does. Traits have the same privilege as the object that uses it, if an attribute is private, the trait will be able to access it. In my playing with traits, I've decided to define attributes specifically used by the trait(fillable
, createdOn
, updatedOn
) on the trait itself. While this hides attributes from the object, it makes the trait clearer. I believe it also makes testing easier to some extent. If you define the attribute in both the trait and the object it will generate a PHP strict error
.
Testing Traits in PHPUnit
Testing for traits can be done in a couple different ways, Florian Wolters has written a good blog post on the subject. PHPUnit >= 3.7 has a function, getObjectForTrait
, which will create classes that implement a trait. The test for DateColumnTrait
could look something like this:
<?php class DateColumnTraitTest extends PHPUnit_Framework_TestCase
{
protected $traitObject;
public function setUp()
{
$this->traitObject = $this->getObjectForTrait('DateColumnTrait');
}
public function testSetCreatedOn()
{
$dt = new DateTime();
$this->traitObject->setCreatedOn($dt->format('Y-m-d H:i:s'));
$this->assertAttributeEquals($dt, 'createdOn', $this->traitObject);
}
}