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. Filters are a collection of anonymous functions that happen in a first-in, first-out order. We stack multiple filters, and a filter can do anything with the chain that comes after.
Here is another simple example that allows us to log the result of the read method.
Filters::add('Database::read', function ($method, $self, $params)) {
$result = Filters::next($method, $self, $params);
file_put_contents('/tmp/log', var_export($result, true), FILE_APPEND);
return $result;
});
Using this technique to separate logging code from the rest of our system also means that rather than being scattered throughout our app, all of our logging code can be organized in one authoritative place. The same can be done with other such concerns including caching, access control, dealing with translations, &c.
Filters provide a large amount of flexibility, and, when used responsibly, can result in much simpler code. We can do anything before or after, or even completely intercept the method in the chain. Of course, the flexibility this approach provides can also lead to increased complexity if we are not responsible. We have to constantly be aware where we are modifying functionality, but side effects are inherent in any system. Still, building in flexibility with a system like filters ultimately keeps the code simpler, longer.
Since I brought up Lithium’s filter system before, you might be
interested in seeing how it compares. The Filters
class is part
of the util
package. Together with the core
package, Filters
could easily be added to any project. In the
example above, the Filters
class holds everything that is
expected to happen on particular method. In Lithium, each class maintains
the stack of filters to be used. Lithium accomplishes this by having any
class with filter methods extend Object
or
StaticObject
. Another difference is the parameters that need
to be passed and calling static methods to achieve the functionality.
Lithium implements Filters
as an Iterator
, so
traversing the stack is much cleaner.
As a parting thought, you might be wondering how I use filters given the scenario I mentioned the beginning. Supposing that we need a particular feature by Sunday, and enabling this feature involves modifying the method of a pre-existing class in the system, I could quickly make the method filterable as was done in the first code example, then add my desired functionality to a shiny new filter. If, at a later point, I recognize that this feature needs to be more fundamental to the system, I can easily refactor the filter into a more concrete implementation.