Thursday, September 24, 2009

A Different Way: Traits in PHP

For a while now I've had some friends who are Perl fanatics. Perl's OOP support is minimal, which makes it extremely flexible and powerful. One module that takes advantage of that is Moose, which provides an easy-to-use framework for OO design. Basically, Moose gives Perl the OO capabilities that most languages take for granted, if that's what you're looking for. In addtion to such capabilities are a few that most languages don't have, such as Roles.

Moose Roles are:

  • like multiple-inheritance, except that a Role handles method and attribute collisions

  • like interfaces, except that a Role is more than a contract--it's fully-working code

  • like mixins, except that Roles can have requirements for being applied to an object

  • like duck typing, except that a Role is a named collection of methods and attributes, so the state of being something is more concrete


These ideas really inspired me, but I'm stuck with PHP. Fortunately, PHP 5.3 comes with a crippled implementation of lambdas and closures, which gives just enough functionality to implement something similar to Moose::Role. I also read these two papers, which gave me some solid ideas, although I didn't read them as gospel.

You can get the code from GitHub.

How It Works

First, you have to define some traits:

TObj::Trait('Sortable',
'sort',function($obj) {
...
},
'sortAttribute','',
'setSortAttribute',function($obj,$attrName) {
$obj->sortAttribute = $attrName;
}
);

TObj::Trait('Categorizable',
'category','',
'getCategory',function($obj) {
return $obj->category;
}
);

Now we create some other class:

class Something extends TObj {
private $foo;

public function __construct($baz) {
$this->foo = $baz;
}
}

And now we apply our traits to Something object:

$something = new Something('lolwut');
$something->apply(Sortable);
$something->apply(Categorizable);

You can now call those traits' methods as if they belonged to the Something class! Note that an object's methods and attributes will override any methods or attributes in the traits, even within the context of the calling trait (see part about aliasing below). This makes traits as flexible as extended super classes.

It's a fairly simple concept, though there are some hang-ups when you get down to the nitty-gritty. For instance, what if two traits have methods or attributes with the same name? Well, you can simply exclude it from one of the traits:

$something->apply(Categorizable,array(
except=>array('getCategory')
)
);

or alias it:

$something->apply(Categorizable,array(
alias=>array('getCategory'=>'getCategoryName')
)
);

But what if getCategory() is used all over in other Categorizable methods? Now they'll actually be calling the getCategory() from the other trait, right?

Wrong! Since TObj has to handle all trait method dispatch, it does a little magic. To resolve method and attribute names, it checks for the original name ("getCategory") within the trait whose method made the call, then moves outside and checks aliased and unaliased names. It works the same way for trait attributes, too. This prevents aliases from breaking trait code.

To know if some random object you've received has a certain trait:

if ($something->applied(Categorizable)) {
echo 'It is categorizable!';
} else {
echo 'This object marches to the beat of a different drummer.';
}

To require that the object have a certain trait method:

TObj::Trait('TraitThatRequires',
required,array('aRequiredMethod'),
...
);

$something->apply(TraitThatRequires);

The last line would throw an exception because aRequiredMethod() is not yet implemented by a trait applied to $something.

TObj traits do other things, and you're welcome to check out its unit tests to see its full functionality.

This project is very new, and it has little or no documentation and I'm sure it has bugs (though all tests pass, of course). If you're interested in the project, feel free to pull it and contact me if you have any interesting ideas.

3 comments:

Sean Huber said...

You should check out http://github.com/huberry/phuby - Ruby style mixins, proc objects with dynamic binding (including $this), and other rubyisms.

Dave Doyle said...

This is awesome. Thank you. I've gotta play with this.

As a Perl guy who loves Moose but has to do a fair bit of PHP I'm hoping this will make my life better.

Lucas Oman said...

Sean, thanks for the tip! This is a really neat project.

Dave, thanks! I hope you find it useful. Although you're not a Ruby guy, you may also want to check out Sean Huber's project in the comment above.