There are always units or features in an application which need to "cut across" many different parts of the architecture at once.
This problem is not really obvious when you are using a "full-featured" framework. Indeed, since your issue is probably a common one, there's a good chance the framework resolved this cross-cutting issue either by sacrificing separation of concerns or by providing an abstraction on top the framework to solve it. A number of frameworks use an event-driven architecture to address the cross-cutting problem. But there's always a point where the framework can't provide the control you need in a specific piece of logic. This is particularly the case if you're using a microframework or prefer building your own one with specialised libraries. The more your application respects the separation of concerns principle, the more your cross cutting needs will come to play a crucial role in your architecture.
Aspect-Oriented programming in PHP
Aspect-Oriented Programming, or AOP, is a programming paradigm that focuses on the organization and modularity of cross-cutting concerns. Use cases can be, for example, ACL, logging, error handling, or caching.
PHP's built-in (internal) assumptions (i.e. when you define a function/constant/class it stays defined forever) make the AOP paradigm difficult to implement.
Li3 was the first one to address the cross cutting issue by offering a filter mechanism to allows to filter method's logic through closures. The Li3's implementation requires manually adding some necessary boilerplate code to make a method filterable. With such a constraint, AOP techniques are limited to filtered methods.
The others well known AOP implementation in PHP are:
The AOP PECL extension is an interesting approach and at the same time a risky gamble since it's a PECL extension which is not a widely supported. The other option is the Go! library which is an AOP implementation which patches PHP code on the fly to allow AOP techniques to be used.
There are other existing implementations, but most of them are workarounds based on top of proxies (to the best of my knowledge), an approach which has a lot of limitations.
A new kid on the block
Automatic code generation is not new in PHP, and is used in a number of libraries like ProxyManager to name one. And thanks to the adoption of Composer, Go! also demonstrates that Just In Time code patching was possible.
If code generation is easy, code patching is a bit more complicated. First, because PHP doesn't provide a built-in code parser, and also because there aren't a lot of libraries addressing the PHP code parsing problem. The best known is the PHP-Parser library. PHP-Parser is a great tool, but the fact it simply ignores whitespace formating in its generated abstract syntax trees makes it problematic to use for some code patching. Indeed, the code to be patched is the real executed code. So if you want to have your backtrace accurate on errors, the line numbers need to be respected in patched files.
So we used the Kahlan's JIT code patcher for this task. Kahlan is a new Unit & BDD test framework which allows stubbing or monkey patching your code directly like in Ruby or JavaScript, thanks to some JIT patching techniques. Under the hood, this library is based on a rudimentary PHP parser but it has proven to be fast and stable enough for our needs here.
The filter library is available at github.com/crysalead/filter and can be used like the following.
First, the JIT code patcher must be initialized as soon as possible (for example just after including the composer autoloade) like so:
include __DIR__ . '/../vendor/autoload.php';
use Lead\Filter\Filters;
Filters::patch(true);
Note that code patching will only work for classes loaded by the Composer autoloader. If a class is included using a require
or include
statement, or has already been loaded before the Filters::patch(true)
call, it won't be patched.
By default all patched code will be stored in the /tmp/jit
folder but you can set your own:
Filters::patch(true, ['cachePath' => 'my/cache/path/jit']);
Cached files will be regenerated automatically every time a PHP file is modified.
Warning! Because Filters::patch(true)
is the no-brainer way to setup the patcher, you should keep in mind that all your code will be patched. Having all methods of your codebase (and vendor code) to be wrapped inside a filter closure can be time-consuming.
Fortunately, if cross cutting concerns play a crucial role in well designed code, it's only required in a couple of methods in a project. So, the prefered approach is to only patch the methods which are going to be filtered:
Filters::patch([
'A\ClassName',
'An\Example\ClassName::foo',
'A\Second\Example\ClassName' => ['foo', 'bar'],
], [
'cachePath' => 'my/cache/path/jit',
]);
That way, you can choose to patch all methods of a specific class, one method only, or just a couple of them.
The Filter API
Now that the JIT patcher is enabled, let's create a logging filter:
use Chaos\Filter\Filters;
use Chaos\Database\Database;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$logger = new Logger('database');
$logger->pushHandler(new StreamHandler('my/log/path/db.log', Logger::WARNING));
Filters::apply(Database::class, 'query', function($next, $sql, $data, $options) use ($logger) {
$logger->info('Ran SQL query: ' . $sql);
return $next($sql, $data, $options);
});
The above example creates a SQL query logger filter for the Chaos database library. More information on the filter API at github.com/crysalead/filter.
Conclusion
AOP is, in my opinion, the true answer for all cross cutting concerns. All other abstractions are at the same time unnecessary and most of the time limited to a specific scope. I have not given up hope that one day PHP will provide a built-in Aspect-Oriented Programming API, but for the moment, I think JIT code patching is the best option, since the benefits largely outperform the CPU overhead which is almost negligible when the JIT code patching is not applied globally.