PHP Traits for Implementing Interfaces
PHP 5.4 introduced traits. Traits are in many ways like code-assisted copy and paste. They are ways to mix code into a class without inheritance. But what are they good for? When should they be used?
Traits are an excellent way to fulfill the requirements of interfaces.
Interfaces
What is an interface? An interface is a contract that the programmer makes with the program to write code in a certain way. Here’s an example:
<?php
interface CRUD {
public function create(array $options, array &$errors);
public function read(array $options, array &$errors);
public function update(array $options, array &$errors);
public function delete(array $options, array &$errors);
}
Here we created a CRUD interface. What this means is any class that implements this interface promises to have these four functions. Generally, you should also make a personal contract that these functions will have the same format for the parameters and return the same type of value. PHP does not enforce this, but you should. Most IDEs will let you document the functions in the interface, and will copy this documentation to your implementations.
/**
* @param array $options An associate array of options.
* Always includes $options['query'].
* @param array $errors An empty array that will hold
* Error objects.
* @return boolean TRUE on success, FALSE on failure.
*/
public function create(array $options, array &$errors);
Implements
Now we write classes that implement the interface.
class Article implements CRUD {
public $table = 'article';
public $db;
/**
* @param array $options An associate array of options.
* Always includes $options['query'].
* @param array $errors An empty array that will hold
* Error objects.
* @return boolean TRUE on success, FALSE on failure.
*/
public function create(array $options, array &$errors) {
$columns = [];
$values = [];
foreach($options['query'] as $key => $value) {
// TODO error checking, validation, etc.
$columns[] = "`$key`";
$values[] = "'$value'";
}
$sql = "INSERT INTO {$this->table} (".implode($key).") VALUES (".implode($value).")";
// DB was set earlier, maybe in the constructor.
if ($this->db->query($sql)) {
return true;
} else {
return false;
}
}
... (the rest of the implementations)
}
Good so far, but then you write the Comment
class, and the User
class, and pretty soon you are copying and pasting the exact same implementation code all over the place!
Well, maybe we can make an abstract class:
abstract class mySQLDocument implements CRUD {
public $table;
public $db;
/**
* @param array $options An associate array of options. Always includes $options['query'].
* @param array $errors An empty array that will hold Error objects.
* @return boolean TRUE on success, FALSE on failure.
*/
public function create(array $options, array &$errors) {
$columns = [];
$values = [];
foreach($options['query'] as $key => $value) {
// TODO error checking, validation, etc.
$columns[] = "`$key`";
$values[] = "'$value'";
}
$sql = "INSERT INTO {$this->table} (".implode($key).") VALUES (".implode($value).")";
// DB was set earlier, maybe in the constructor.
if ($this->db->query($sql)) {
return true;
} else {
return false;
}
}
... (the rest of the implementations)
}
class Article extends mySQLDocument {
public $table = 'article';
}
class User extends mySQLDocument {
public $table = 'user';
}
That’s looking great! Code is being reused. And everything is going swell. Then you decide, hey, let’s also store articles in Solr, for fast fulltext searching. Now you have to override the methods in Article
.
class Article extends mySQLDocument {
public $table = 'article';
public function create(array $options, array &$errors) {
$success = parent::create($options, &$errors);
global $solrClient;
$doc = new SolrInputDocument();
foreach($options['query'] as $key => $value) {
$doc->addField($key, $value);
}
try {
$solrClient->addDocument($doc);
return true;
} catch (SolrClientException $e) {
$errors[] = new Error('Solr Error');
return false;
}
}
}
But what happens when you decide articles and comments should be in Solr? You end up copying and pasting. Maybe you decide to make a class called MysqlWithSolr
and extend that. But that is where things start getting messy. In comes out new savior: Traits!
Traits
Traits let you mix code into your classes. Think of it as run-time copy and paste. I’ll start with an example and work from there. In this example, we will just use create.
<?php
interface CRUD {
/**
* @param array $options An associate array of options. Always includes $options['query'].
* @param array $errors An empty array that will hold Error objects.
* @return boolean TRUE on success, FALSE on failure.
*/
public function create(array $options, array &$errors);
}
trait mySQLCrud {
abstract public function getMysqlDB();
abstract public function getMysqlTable();
public function create(array $options, array &$errors) {
$columns = [];
$values = [];
foreach($options['query'] as $key => $value) {
$columns[] = "`$key`";
$values[] = "'$value'";
}
$sql = "INSERT INTO ".$this->getMysqlTable()
." (".implode($key).") VALUES (".implode($value).")";
if ($this->getMysqlDB()->query($sql)) {
return true;
} else {
$errors[] = new Error('MySQL Error');
return false;
}
}
}
trait solrCrud {
abstract public function getSolrClient();
public function create(array $options, array &$errors) {
$client = $this->getSolrClient();
$doc = new SolrInputDocument();
foreach($options['query'] as $key => $value) {
$doc->addField($key, $value);
}
try {
$client->addDocument($doc);
return true;
} catch (SolrClientException $e) {
$errors[] = new Error('Solr Error');
return false;
}
}
}
class User implements CRUD {
use mySQLCrud;
public function getMysqlDB(){
global $db; // Initialized Elsewhere
return $db;
}
public function getMysqlTable() {
return 'user';
}
}
class Article implements CRUD {
use mySQLCrud, solrCrud {
mySQLCrud::create as mysqlCreate;
solrCrud::create as solrCreate;
}
public function getSolrClient(){
global $solr; // Initialized Elsewhere
return $solr;
}
public function getMysqlDB(){
global $db; // Initialized Elsewhere
return $db;
}
public function getMysqlTable() {
return 'user';
}
public function create(array $options, array &$errors) {
$success = $this->mysqlCreate($options, $errors);
if ($success) {
$success = $this->solrCreate($options, $errors);
}
return $success;
}
}
First we set up our interface. We are promising that our CRUD documents will behave the same no matter if the implementation is Solr, MySQL, MongoDB, or carving into stone tablets. The program does not care where the data is store, just that it will be. It will call $success = $article->create($options, $errors)
and let the implementation take care of the details.
Next we make a trait for implementing the CRUD interface in MySQL. We declare two abstract functions, one for getting a DB connection, and the other for getting the name of a table. Making these into abstract functions forces a class to implement them when they use the trait. That way, we can guarantee that a DB connection and table name will be available to the create method, which we write out next.
Now we make a trait for implementing the CRUD interface in Solr. Just like above, we have an abstract function to guarantee a connection to Solr.
Next we declare the User
class. It implements the CRUD interface, and uses the mySQLCrud
trait. This means it must implement the getMysqlDB
and getMysqlTable
functions. But that’s it. The create method is supplied by the trait.
Finally, we get to the Article
class. It too implements CRUD, but it needs both MySQL and Solr. So it simply uses both traits. The issue is that the traits have methods with the same name, so we rename them in the use statement. Now that they are renamed, Article
must implement the create method on its own. In our case, it calls the renamed functions, so that it can store the data in MySQL and Solr.
Conclusions
Traits are a great way to implement interfaces. They are especially good for CRUD interfaces where data is stored in different types of database, and some objects may be stored in more than one.
- Related Topics
Mark
Aug 21, 2013
Reply