PHP 5.4alpha1 was released the other day with some cool new features. One of them is an implementation of traits. From the RFC:
Traits is a mechanism for code reuse in single inheritance languages such as PHP. A Trait is intended to reduce some limitations of single inheritance by enabling a developer to reuse sets of methods freely in several independent classes living in different class hierarchies. The semantics of the combination of Traits and classes is defined in a way, which reduces complexity and avoids the typical problems associated with multiple inheritance and Mixins.
There are some nice examples over at the RFC, and I will provide another example in this post.
But first, to be able to play with this we need to build PHP. It’s the usual ./configure; make; make test;
dance so it should not pose too many problems. The make test
step is optional, but every time you skip that step, God creates another Perl programmer, so please, make test
. If you run into any problems building PHP you will have to look elsewhere for answers. I won’t cover that in this post.
Now that you have built PHP, lets start writing some code.
When working on a framework (or something else for that matter), you will most likely need to introduce parameters for some of your classes, be it database adapters or a storage adapter. All these classes will most likely have the following methods:
- setParams(array $params)
- getParams()
- setParam($key, $value)
- getParam($key)
The implementation of these methods will most likely be the same. If you put the actual implementation of these methods in many classes you’re not DRY anymore. Since PHP does not support multiple inheritance you will have to make a base class with the implementation of these methods, and have other classes ultimately extend this base class. Now you are relatively DRY, but this solution is not optimal(and sometimes not possible at all).
Enter traits, the newest superhero on the PHP street, or something like that.
Now, I’ll create two classes that needs to support parameters; Cogo\Database\MySQL
and Cogo\Storage\Filesystem
. They will each extend Cogo\Database
and Cogo\Storage
respectively that will have the parameter-related methods.
<?php
namespace Cogo;
abstract class Database {
private $params;
public function __construct(array $params) {
$this->setParams($params);
}
private function setParams(array $params) {
$this->params = $params;
}
public function getParams() {
return $this->params;
}
protected function setParam($key, $value) {
$this->params[$key] = $value;
}
public function getParam($key) {
return (isset($this->params[$key]) ? $this->params[$key] : null);
}
protected function someDatabaseRelatedMethod() {
// ...
}
abstract protected function someOtherDatabaseRelatedMethod();
}
<?php
namespace Cogo\Database;
use Cogo\Database;
class MySQL extends Database {
protected function someOtherDatabaseRelatedMethod() {
// ...
}
}
<?php
namespace Cogo;
abstract class Storage {
private $params;
public function __construct(array $params) {
$this->setParams($params);
}
public function setParams(array $params) {
$this->params = $params;
}
public function getParams() {
return $this->params;
}
public function setParam($key, $value) {
$this->params[$key] = $value;
}
public function getParam($key) {
return (isset($this->params[$key]) ? $this->params[$key] : null);
}
protected function someFilesystemRelatedMethod() {
// ...
}
abstract protected function someOtherFilesystemRelatedMethod();
}
<?php
namespace Cogo\Storage;
use Cogo\Storage;
class Filesystem extends Storage {
protected function someOtherFilesystemRelatedMethod() {
// ...
}
}
As you can see the parameter-related methods are almost the same. The difference is that the Cogo\Database
class wants to have its set
methods private
and protected
.
One possibility would be to create a base class for the parameter handling, and have the two abstract classes extend that one, but because multiple inheritance is not possible in PHP it’s easy to grind to a halt using this solution. In the end you might end up with a huge base class with loads of logic simply because it has some logic that you want to share across many other classes.
The base class would also have to have private
on the set methods since you can’t override a method with a stricter visibility, only the other way around.
Using traits, this can be solved in a more elegant fashion. Traits also support changing visibility of the methods defined in the trait, so Cogo\Database
can keep its private
and protected
implementations. Here is how the trait can look like:
<?php
namespace Cogo;
trait ParamsHandler {
private $params;
public function __construct(array $params) {
$this->setParams($params);
}
public function setParams(array $params) {
$this->params = $params;
}
public function getParams() {
return $this->params;
}
public function setParam($key, $value) {
$this->params[$key] = $value;
}
public function getParam($key) {
return (isset($this->params[$key]) ? $this->params[$key] : null);
}
}
Notice that the trait now contains all logic regarding parameters (including the $params
property and the constructor). The RFC states that:
Since Traits do not contain any state/properties …
This seems to work after all. The RFC was created a while ago and it no longer matches the actual implementation 100% obviously.
Now that we have a trait with the logic we want in both our abstract classes, let’s use
it:
<?php
namespace Cogo;
abstract class Database {
use ParamsHandler {
setParams as private;
setParam as protected;
}
protected function someDatabaseRelatedMethod() {
// ...
}
abstract protected function someOtherDatabaseRelatedMethod();
}
<?php
namespace Cogo;
abstract class Storage {
use ParamsHandler;
protected function someFilesystemRelatedMethod() {
// ...
}
abstract protected function someOtherFilesystemRelatedMethod();
}
As you can see we have replaced the implementation of the parameter methods from both abstract classes with a couple of use
statements. In the Cogo\Storage
class we have used the methods from the trait as-is, which means they are all public
. In the Cogo\Database
class we have changed the visibility of two methods to match the initial implementation. We have now managed to keep ourselves DRY, as well as not creating a base class that the two abstract classes extends.
This is just a simple example of how to use traits. They can do loads more, and there are issues like for instance conflict resolution which is not mentioned in this post at all. I will probably post more examples of traits (and perhaps other 5.4 features) later on. In the meantime you should read the RFC, and try it out for yourself.
You will find all the code mentioned in this post over at my php-traits-examples repository at GitHub. Feel free to play around with it, and if you have other examples on how to use traits please fork the project and send me a pull request so I can include it (and learn from it).