Aspects of Love — How deep does the rabbit hole go?

If you’ve read my previous post (Discharging Static #1), then you’ll know that recently I’ve been exploring different approaches to creating test doubles for statically called methods, and other testing problems, and had begun to look at Michael Bodnarchuk’s AspectMock library as an alternative to Mockery. Much as I like Mockery, and have no issues with it when tests are run standalone, it can be problematic when running the whole test suite; whether upstream tests have already autoloaded the classes that you need to mock, or breaking downstream tests, even when calling Mockery::close() as part of the PHPUnit tearDown().

AspectMock is a stubbing/mocking library written as part of the Codeception test framework, but which will also work with PHPUnit; and which provides functionality that not only allows mocking of both static and instance methods, but can also mock functions (including PHP’s built-in functions, as well as userland functions). It can achieve all this because it is built on the The Go! AOP framework for PHP. Exploring how this worked introduced me to the AOP (Aspect Oriented Programming) paradigm; and I feel like I’ve taken the red pill, followed the white rabbit down the hole, and entered the surreality of Wonderland.

First of all, it’s probably worth mentioning that Aspect Oriented Programming (AOP) isn’t an alternative programming paradigm to OOP (in the way that procedural programming or functional programming are), but a supplement to it. The main focus and purpose of Aspect Oriented Programming is separation of concerns. System services such as security and audit, logging, caching, and transaction management can be maintained independently from the business logic of the application. Such system services are referred to as cross-cutting concerns, because they cut across many aspects of the application, and across many business and domain models. Maintaining these services independently of the business models is an approach that leads to cleaner code: the methods of your codebase are no longer filled with security checks and logging calls, they contain nothing more than the actual business logic.

AOP encapsulates these cross-cutting concerns into “Aspects”, and an Aspect can apply an “Advice” (a specific behaviour or piece of code logic) at “Join Points”, points within the script where the additional behaviour can be joined. A Join Point might be before or after the execution of a particular method, on access to a specified property, or on an exception. Join Points are identified by a “Pointcut”, an expression (perhaps based on a regular expression that matches class/method names) that identifies one or more Join Points where the Advice should be executed. (Like all programming paradigms, AOP has its own unique terminology and jargon.)

AOP

In principle, this is similar to a technique that I used many years ago applying pre- and post-hooks (or before and after hooks) to method calls; but back then, every method required a first and last line that would check to see if any “hooks” (join points) existed, and (if so) to then call the hook (advice) code. But AOP uses a rather more sophisticated and formalised approach. There is no need to include any code in the business objects to check for Aspects; instead, an “Aspect Weaver” identifies the join points, and modifies the application code at compile time (or at the point where composer or an autoloader includes the class in the case of Go!) to include the Advice.

Add new methods to your business domains, and Advices will be automatically applied if they match existing pointcut expressions (useful when a security Advice must be called before every public method in our controllers to validate access privileges); modify a security Aspect, and that change is automatically reflected in all methods that match the pointcut; modify a logging pointcut definition, to enable logging in more methods.


This AOP approach isn’t without some potential problems:

  • An Advice must be fairly generic if it can potentially be applied to any join point in a codebase, and while AOP can glean some information from the join point that allows its behaviour to be customised, that is beginning to create hard-dependencies. So a logger might record the request arguments passed to every public method in every controller, but we don’t want user passwords to be written to the logs, so we need to start creating specific exceptions in generic Advices, or a number of different Advices that must be applied to specific joins.
  • It is not obvious when working with the business logic of our application what Aspects (if any) will be applied to the code when it runs, and at what point they will be run (before, after, around), or the order in which multiple Advices might run. We could add annotations in the docblocks, but those can become out-of-date very quickly if a pointcut is changed, and so couldn’t be trusted.
  • Changes to methods or property names in the business code might result in code being covered by a pointcut when it wasn’t previously, or being excluded when it was previously included, with unintended consequences.
  • If configured carelessly, then pointcuts can be applied to Aspects as well as to the main business logic code, leading to horrors like a logger method calling itself recursively whenever it is called.

Hopefully, the risks from these potential issues could be mitigated by good developer education and naming standards, and any technical problems should be picked up during integration testing; but they are problems with using AOP, and the whole development team needs to be aware of them. When choosing to use AOP for a project, it’s probably best for “greenfield” projects, with the entire team onboard from the outset, and designing with AOP fully in mind: separation of concerns applies to the code building, and not to team knowledge.

But there are benefits to AOP beyond those primary objectives of keeping business logic clean from cross-cutting concerns, and maintaining a separation of concerns.

  • It can simplify unit testing of the business code, because we no longer need to mock objects like loggers or security checks that are handled by Aspects (although the Advices themselves would also need to be independently unit tested, and integration testing needs to cover all Aspects as well as business functionality).
  • When onboarding a legacy project, applying a Trace/Logger Aspect could help familiarise a developer with the logical paths through that codebase for individual request.
  • With a spaghetti legacy project, refactoring cross-cuts such as logging and caching to Aspects could help improve the readability of the code by gradually reducing it to just the coree business logic… while not a permanent solution, it does reduce the complexities of the codebase, allowing for an easier subsequent rewrite.

In these latter cases, AOP is an aid to rewriting the legacy code, not necessarily a final objective for the rewrite.


PHP has its own implementation of an AOP framework, the Go! framework written by Alexander Lisachenko, and that can be used in almost any application, with almost any framework to provide transparent hooking for cross-cutting. It’s this framework that provides the mechanism to patch autoloaded classes “on the fly” in order to mock them in AspectMock.

Using Go! we can define Aspects as a class implementing Go\Aop\Aspect, and with methods that define the code logic for logging or caching or whatever functionality we want to implement, with annotations such as @Before, @Around, or @After used to define the Join Points and Pointcuts. (There are also annotations for intercepting thrown exceptions, and join points that can control access to properties as well as methods, and for class definitions to be modified further with interfaces or additional traits.) An order of execution can be specified in these annotations, for those cases where multiple Join Points all reference the same class/method.

As an example:


namespace System\Aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;

class LoggingAspect implements Aspect
{
    /**
     * Logger Advice for Public Setter methods in any class
     *
     * @param MethodInvocation $invocation
     * @Before("execution(public **->set*(*))", order=-128)
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        Log::info('Calling Before Interceptor for ' .
             $invocation .
             ' with arguments: ' .
             json_encode($invocation->getArguments());
    }
}

The MethodInvocation object passed to the Advice contains all details of the class and method call (including arguments, etc) that is intercepted by the Aspect, so the information is available to use within the code of our Advice.

Go! works by hooking into the autoloader – it will not work with files that are manually included or required without using an autoloader – and intercepting all calls to load class definitions through that; so the “Aspect Kernel” that manages the framework needs to be initialised before any classes have been loaded by a request, preferably immediately after the vendor autoload. Then, as classes are requested through the autoloader, it reads the class file (without including it) and builds its own definition of that class, renaming it as proxied (so UserController would become UserController_AopProxied), and creates a new procy UserController class definition that extends the newly renamed UserController_AopProxied class, and that implements methods to invoke the Advices for the appropriate Join Points, as well as the list of Advices that should be invoked. It is this new UserController class that will then be used within the application;. and it is this pair of modified class definitions that are then loaded.

When we initialise the Aspect Kernel, we need to configure a cache folder (where the modified class definitions will be stored), and set “include” and “exclude” directories that define which business code we want to set Aspects for.


use System\Aspect\ApplicationAspectKernel;

include __DIR__ . '/../vendor/autoload.php';

ApplicationAspectKernel::getInstance()
    ->init([
        'debug' => true,
        'appDir' => __DIR__ . '/../businessCode',
        'cacheDir' => __DIR__ . '/cache',
    ]);

The Aspect Kernel is also where all the Aspects that we want to apply to the code are “registered”, so we need to add each aspect manually to that.


use Go\Core\AspectKernel;
use Go\Core\AspectContainer;
use System\Aspect\LoggingAspect;
use System\Aspect\CacheingAspect;

/**
 * Application Aspect Kernel
 */
class ApplicationAspectKernel extends AspectKernel
{

    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new LoggingAspect());
        $container->registerAspect(new CacheingAspect());
    }
} 

Once Aspects are registered, the Aspect Kernel will automatically rebuild all referenced classes (as defined by the Pointcuts and Join Points defined for that Aspect) on the next request. If you’re using OpCaching, then it will be the AOP-modified files in the cache folder that are stored in OpCache.


I know developers that dislike any form of “magic” in their code, for whom even PHP’s simple built-in magic methods like __get and __set() are anathema, and for whom closure binding is a dark alchemy. The voodoo of AOP and Go! is certainly not something for them.

Perhaps AOP isn’t an appropriate tool for production sites — the jury will be out for a long time deliberating on that issue — but I won’t dismiss a potentially useful tool or approach to programming just because it might be considered “magic”. AspectMock has already demonstrated how useful it is using AOP and Go!, allowing me to create test doubles that Mockery or PHPUnit couldn’t mock; and now I want to see just how deep the rabbit hole goes.

Advertisements
This entry was posted in PHP and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s