PhpRiot
News Archive
PhpRiot Newsletter
Your Email Address:

More information

Aspect-Oriented Design

Note: This article was originally published at Planet PHP on 23 December 2010.
Planet PHP

I try to write simple apps. I try to code for today, as Lorna articulated so well. Almost invariably, I end up with something more complex than I intended. One thing that has helped immensely is unit testing. Not only does it help me reduce complexity, it provides flexibility, which is almost as important.

Flexibility lets me adapt to the changing requirements that are often the cause of more complex code. I need a particular feature by the end of the day on Sunday, so I'll just write a class that extends the current functionality. Maybe I actually have to replace more functionality than extending and overriding will allow, so I will add in a conditional to load a totally new class. Whatever the case, changes are happening so fast that architecting a smarter approach makes little sense on Saturday night.

I tried to solve this problem once before by adding in methods of the parent class that would get called, but nothing would actually happen unless the child implemented them. The result was a callback system that allowed me to add in functionality, but it never allowed the underlying method to change.

Luckily, I have this buddy Nate who found himself in a similar predicament. He wanted more flexibility, he wanted simpler code, and he was not happy with the callback approach. At about the same time, PHP introduced some new functionality in the form of anonymous functions. With new tools in hand, a knowledge of aspect-oriented programming, and a deep desire for more flexibility, a new approach called method filters developed. This is one of the software architectures that powers our new framework, Lithium.

Aspect-oriented programming desires to solve the problem of injecting functionality into concrete methods of a class. This approach allows us to extend or replace individual methods without having to actually extend the class itself. The intention is to take disparate programming concerns, or aspects, and disentangle them from the business logic of our code. While the concept of AOP itself provides a great deal of flexibility, traditional implementations involve whole new layers, complete with new and unintuitive terminology, and code preprocessing. In other words, not too simple.

The filters approach, on the other hand, gives us both flexibility and simplicity. Below is a simple example of a possible Filters class. This class mimics the behavior of the one found in Lithium, but with much less elegance and robustness.

class Filters {protected static $_filters = array(); public static function add($method, $callback) { static::$_filters[$method][] = $callback; } public static function run($method, $object, $params, $filter) { static::add($method, $filter); return static::next($method, $object, $params); } public static function next($method, $object, $params) { $next = array_shift(static::$_filters[$method]); return $next($method, $object, $params); } }

The add method allows us to stack anonymous functions as filters. The run method is used by class methods that should be filterable. The next method returns the result of the next filter in the stack. The run and next methods both accept the instance of the class to be filtered and the parameters that were passed to the class's original method. These parameters are then passed into the anonymous function, so we have access to everything in the original method.

To take advantage of this Filters class, we simply have to wire our methods to accept these parameters. Below, I create a basic class that might interact with the database. It has a read method that we want to be filterable. To do this, we pass the method signature, Database::read, represented by the __METHOD__ constant, then pass the current instance, the parameters of the original method, and finally a closure for the basic functionality.

class Database { public function read($query, array $options = array()) { $params = compact('query', 'options'); return Filters::run(___METHOD__, $this, $params, function ($method, $self, $params) { $result = 'the basic method functionality'; return $result; }); } }

Now that the read method is filterable, we might want to add some logging. Below is an example filter that attaches the anonymous function to the Database::read method.

Filters::add('Database::read', function ($method, $self, $params)) { file_put_contents('/tmp/log', var_export($params, true), FILE_APPEND); return Filters::next($method, $self, $params); });

These two pieces of code provide interesting insight into how filters work.

Truncated by Planet PHP, read more at the original (another 2782 bytes)