Getting rid of duplicate end-to-end code in PHP: code refactoring with AOP

I think that every programmer is familiar with the principle of sole responsibility, because it exists for nothing: observing it, you can write code better, it will be more understandable, it will be easier to modify.

But the more each of us works with the code, the more comes the understanding that at the existing level of the language - object-oriented - this can not be done. But the fact of end-to-end functionality prevents us from observing the principle of sole responsibility.

This article is about how to get rid of duplicate pass-through code, and how to make it a little better with AOP.



End-to-end functionality or wet code


With a probability of about 95% in any application, you can find pieces of end-to-end functionality that are hidden in the code under the guise of caching, logging, exception handling, transactional control and access rights differentiation. As you guessed from the name, this functionality lives on all layers of the application (do you have layers?) And forces us to break several important principles: DRY and KISS . Violating the DRY principle, you automatically start using the WET principle and the code becomes “wet”, which is reflected in the form of an increase in the Lines of Code (LOC), Weighted Method Count (WMC), Cyclomatic Complexity (CCN) metrics .

Let's see how this happens in real life. The technical task arrives, a system is designed according to it, decomposition into classes is carried out, and the necessary methods are described. At this stage, the system is perfect, the purpose of each class and service is clear, everything is simple and logical. And then the end-to-end functionality begins to dictate its own rules and forces the programmer to make changes to the code of all classes, since in OOP there is no way to decompose the end-to-end functionality. This process proceeds unnoticed, because everyone is used to it, as a normal phenomenon, and no one is trying to fix anything. The process goes according to the standard scheme, worked out over the years.

First, the logic of the method itself is written, which contains the necessary and sufficient implementation:

/**
 * Creates a new user
 *
 * @param string $newUsername Name for a new user
 */
public function createNewUser($newUsername)
{
    $user = new User();
    $user->setName($newUsername);
    $this->entityManager->persist($user);
    $this->entityManager->flush();
}


... after that we add 3 more lines of code for checking access rights

/** ... */
public function createNewUser($newUsername)
{
    if (!$this->security->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }
    $user = new User();
    $user->setName($newUsername);
    $this->entityManager->persist($user);
    $this->entityManager->flush();
}


... then 2 more lines for logging the beginning and end of the method

/** ... */
public function createNewUser($newUsername)
{
    if (!$this->security->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }
    $this->logger->info("Creating a new user {$newUsername}");
    $user = new User();
    $user->setName($newUsername);
    $this->entityManager->persist($user);
    $this->entityManager->flush();
    $this->logger->info("User {$newUsername} was created");
}


Do you recognize your code? Not yet? Then let's add there another 5 lines of processing a possible exception with several different handlers. In methods that return data, another 5 lines can be added to save the result in the cache. Thus, from 4 lines of code that really have value, you can get about 20 lines of code. What this threatens, I think it’s clear - the method becomes more complicated, it is harder to read, it takes longer to figure out what it really does, it is harder to test, because you have to slip mocks for the logger, cache, etc. Since the example was for one of the methods, it is logical to assume that statements regarding the size of the method are true for the class and for the whole system. The older the system code, the more it grows like garbage and it becomes harder to keep track of it.

Let's look at existing solutions to cross-cutting functionality problems.

Cleanliness is the key to health! Health first!


I recommend reading the title in the context of application development as follows: “Code cleanliness is the key to the health of the application! The health of the application is first! ” It would be nice to hang such a sign in front of each developer to always remember this :)

So, we decided to keep the code clean. What solutions do we have and what can we use?

Decorators


Decorators are the first thing that comes to mind. Decorator is a structural design template designed to dynamically connect additional behavior to an object. The Decorator pattern provides a flexible alternative to the practice of subclassing to extend functionality.

When it comes to AOP , the first question that OOP programmers usually ask is, why not use a regular decorator? And it is right! Because the decorator can do almost everything that is done using AOP, but ... A counter-example: what if we make LoggingDecorator on top of CachingDecorator, and the latter, in turn, on top of the main class? How many similar code will be in these decorators? How many different classes of decorators will be in the whole system?

It is easy to estimate that if we have 100 classes that implement 100 interfaces, then adding caching decorators will add another 100 classes to our system. Of course, this is not a problem in the modern world (look in the cache folder of any large framework), but why do we need these 100 classes of the same type? It’s not clear, agree?

However, moderate use of decorators is fully justified.

Proxy classes


Proxy classes are the second thing that comes to mind. Proxy - a design pattern, provides an object that controls access to another object, intercepting all calls (it acts as a container).

Not a good solution from my point of view, but all developers have heard proxy caches, so they can be found so often in applications. The main disadvantages: a drop in the speed of work (__call, __get, __callStatic, call_user_func_array is often used), and also typinghint is broken, because instead of the real object a proxy object comes. If you try to wrap the caching proxy on top of the logging one, and that one, in turn, on top of the main class, the speed will drop by an order of magnitude.

But there is a plus: in the case of 100 classes, we can write one caching proxy for all classes. But! At the cost of rejecting typinghinting on 100 interfaces, which is categorically unacceptable in the development of modern applications.

Events and Observer Pattern


It is hard not to recall such a wonderful pattern as the Observer. Observer is a behavioral design pattern. Also known as Dependents, Publisher-Subscriber.

In many well-known frameworks, developers are faced with cross-cutting functionality and the need to expand the logic of a method over time. Many ideas were tried, and one of the most successful and understandable was the model of events and subscribers to these events. By adding or removing subscribers to events, we can expand the logic of the main method, and by changing their order using priorities, we can execute the logic of the handlers in the desired order. Very good, almost AOP!

It should be noted that this is the most flexible template, since on its basis you can design a system that will expand very easily and will be understandable. If there were no AOP, it would be the best way to extend the logic of methods without changing the source code. Not surprisingly, many frameworks use events to extend functionality, for example, ZF2, Symfony2. The Symfony2 website has a great article on how you can extend the logic of a method without using inheritance.

Nevertheless, despite all the advantages, there are several big disadvantages that sometimes outweigh the advantages. The first minus is that you must know in advance what and where can expand in your system. Unfortunately, this is often unknown. The second minus is that you need to write code in a special way, adding template lines for generating an event and processing it (example from Symfony2):

class Foo
{
    // ...
    public function __call($method, $arguments)
    {
        // create an event named 'foo.method_is_not_found'
        $event = new HandleUndefinedMethodEvent($this, $method, $arguments);
        $this->dispatcher->dispatch('foo.method_is_not_found', $event);
        // no listener was able to process the event? The method does not exist
        if (!$event->isProcessed()) {
            throw new \Exception(sprintf('Call to undefined method %s::%s.', get_class($this), $method));
        }
        // return the listener returned value
        return $event->getReturnValue();
    }
}


Signals and Slots


This pattern , in its essence, is an implementation of the Observer pattern, but it allows reducing the number of repeated code.

Of the most interesting implementations of this pattern, I would mention the core of the Lithium framework, the study of which can give a lot of new things even to advanced developers. In short, Lithium allows you to attach callback filter functions to any important methods in the system and perform additional processing. If you want to write a log of database queries to a file in debug mode - there is nothing easier:

use lithium\analysis\Logger;
use lithium\data\Connections;
// Set up the logger configuration to use the file adapter.
Logger::config(array(
    'default' => array('adapter' => 'File')
));
// Filter the database adapter returned from the Connections object.
Connections::get('default')->applyFilter('_execute', function($self, $params, $chain) {
    // Hand the SQL in the params headed to _execute() to the logger:
    Logger::debug(date("D M j G:i:s") . " " . $params['sql']);
    // Always make sure to keep the filter chain going.
    return $chain->next($self, $params, $chain);
});


I strongly recommend that you familiarize yourself with the filter system , because the implementation of filters in Lithium brings the development to aspect-oriented programming as much as possible and can be the impetus for you that will plunge into the world of AOP completely.

Aspect Oriented Programming


So, we came to the most interesting thing - to use aspect-oriented programming to combat duplication of end-to-end code. There were already articles on AOP on the Habr , including for PHP , so I will not repeat this material and give definitions of those terms and those techniques that AOP uses. If you are not familiar with the terms and concepts of AOP, then before further reading you can read the article on AOP on Wikipedia.

So, filters in Lithium allow you to connect additional handlers almost anywhere, which makes it possible to move the caching, logging, and access control codes into separate closures. It would seem that here it is, a silver bullet. But everything is not so smooth. Firstly, to use filters, we need to connect the entire framework, since there is no separate library for this, but it's a pity. Secondly, closure filters (in terms of AOP - tips) are scattered everywhere and it is very difficult to follow them. Thirdly, the code must be written in a certain way and implement special interfaces so that filters can be used. These three disadvantages significantly limit the ability to use filters as AOP in other applications and frameworks.

This is where I got the idea - to write a library that would make it possible to use AOP in any PHP application. Then there was the battle with PHP, the study of code acceleration techniques, the fight against opcode accelerator bugs, and many, many interesting things. As a result, the Go library was born! AOP PHP, which can be embedded in an existing application, intercept available methods in all classes and extract from them through functionality over several thousand lines of code into a couple of dozen lines of tips.

Go library! AOP PHP


The main differences from all existing analogues is a library that does not require any PHP extensions and does not call black magic runkit-a and php-aop for help. It does not use eval, is not tied to a DI container, and does not need a separate aspect compiler for the final code. Aspects are ordinary classes, organically using all the features of OOP. The code generated by the library with interwoven aspects is very clean, it can be easily debugged using XDebug, both the classes themselves and the aspects.

The most valuable thing in this library is that theoretically it can be connected to any application, because to add new functionality using AOP you do not need to change the application code at all, aspects are intertwined dynamically. For example: using ten to twenty lines of code, you can intercept all public, protected and static methods in all classes when starting a standard ZF2 application and display the name of this method and its parameters when calling the method.

The issue of working with the opcode cache has been worked out - in combat mode, the weaving of aspects occurs only once, after which the code is taken from the opcode cache. Doctrine annotations for aspect classes are used. In general, a lot of interesting things inside.

Pass-through code refactoring using AOP


To rekindle interest in AOP more, I decided to choose an interesting topic about which you can find little information - code refactoring to aspects. Then there will be two examples of how you can make your code cleaner and more understandable using aspects.

We take out logging from the code

So, let's imagine that we have logging of all executable public methods in 20 classes located in the Acme namespace. It looks something like this:

namespace Acme;
class Controller
{
    public function updateData($arg1, $arg2)
    {
        $this->logger->info("Executing method " . __METHOD__, func_get_args());
        // ...
    }    
}


Let's take and refactor this code using aspects! It is easy to notice that logging is done before the code of the method itself, so immediately select the type of advice - Before. Next, we need to determine the point of implementation - the implementation of all public methods inside the Acme namespace. This rule is defined by the expression execution (public Acme \ * -> * ()). So, we write LoggingAspect:

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Before;
/**
 * Logging aspect
 */
class LoggingAspect implements Aspect
{
    /** @var null|LoggerInterface */
    protected $logger = null;
    /** ... */
    public function __construct($logger) 
    {
        $this->logger = $logger;
    }
    /**
     * Method that should be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Acme\*->*())")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        $obj    = $invocation->getThis();
        $class  = is_object($obj) ? get_class($obj) : $obj;
        $type   = $invocation->getMethod()->isStatic() ? '::' : '->';
        $name   = $invocation->getMethod()->getName();
        $method = $class . $type . $name;
        $this->logger->info("Executing method " . $method, $invocation->getArguments());
    }
}    


Nothing complicated, a regular class with a usual-looking method. However, this is an aspect that defines the beforeMethodExecution advice that will be called before calling the methods we need. As you already noticed, Go! uses annotations to store metadata, which has long been common practice, as it is visual and convenient. Now we can register our aspect in the Go core! and throw all the logging out of the heap of our classes! Having removed the unnecessary dependence on the logger, we made our class code cleaner, it began to observe the principle of single responsibility more, because we took out from it what it should not do.

Moreover, now we can easily change the logging format, because now it is set in one place.

Transparent caching

I think everyone knows the boilerplate method code using caching:

    /** ... */
    public function cachedMethod()
    {
        $key = __METHOD__;
        $result = $this->cache->get($key, $success);
        if (!$success) {
            $result = // ...
            $this->cache->set($key, $result);
        }
        return $result;
    }


Undoubtedly, everyone will recognize this boilerplate code, as there are always enough places. If we have a large system, then there can be a lot of such methods, so make sure that they themselves are cached. What an idea! Let's annotate those methods that should be cached, and set the condition in the instantiate - all methods marked with a specific annotation. Since caching “wraps” the method code, we also need the appropriate type of advice - Around, the most powerful. This type of advice decides on the need to execute the source code of the method. And then everything is simple:

use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\Around;
class CachingAspect implements Aspect
{
    /**
     * Cache logic
     *
     * @param MethodInvocation $invocation Invocation
     * @Around("@annotation(Annotation\Cacheable)")
     */
    public function aroundCacheable(MethodInvocation $invocation)
    {
        static $memoryCache = array();
        $obj   = $invocation->getThis();
        $class = is_object($obj) ? get_class($obj) : $obj;
        $key   = $class . ':' . $invocation->getMethod()->name;
        if (!isset($memoryCache[$key])) {
            $memoryCache[$key] = $invocation->proceed();
        }
        return $memoryCache[$key];
    }
}


In this tip, the most interesting thing is to call the original method, which is done by calling proceed () on the MethodInvocation object, which contains information about the current method. It is easy to see that if we have data in the cache, then we do not call the original method. At the same time, your code does not change in any way!
Having this aspect, we can annotate Annotation \ Cacheable before any method and this method will be cached automatically thanks to AOP. We go through all the methods and cut out the caching logic, replacing it with an annotation. Now the boilerplate method code using caching looks simple and elegant:

    /** 
     * @Cacheable
     */
    public function cachedMethod()
    {
        $result = // ...
        return $result;
    }


This example can also be found inside the demos folder of the Go! Library AOP PHP, and also look at the commit that implements the above in action.

Conclusion


Aspect-oriented programming is a fairly new paradigm for PHP, but it has a great future. The development of metaprogramming, the writing of Enterprise frameworks in PHP - all this follows in the wake of Java, and AOP in Java has been living for a very long time, so you need to prepare for AOP right now.

Go! AOP PHP is one of the few libraries that works with AOP and in some matters compares favorably with its analogues - the ability to intercept static methods, methods in final classes, access object properties, and debug source code and aspect code. Go! uses a lot of techniques to ensure high performance: compilation instead of interpretation, lack of slow techniques, optimized execution code, the ability to use an opcode cache - all this contributes to the common cause. One of the amazing discoveries was that Go! in some similar conditions, it can run faster than C-extension PHP-AOP. Yes, it’s true, which has a simple explanation - the extension interferes with the work of all methods in PHP in runtime and does small checks for compliance with the point, the more such checks the slower the call of each method, whereas Go! does this once when compiling the class code, without affecting the speed of the methods in runtime.

If you have questions and suggestions about the library, I will be happy to discuss them with you. I hope my first article on Habré was useful to you.

References

  1. Source code https://github.com/lisachenko/go-aop-php
  2. Presentation of SymfonyCampUA-2012 http://www.slideshare.net/lisachenko/php-go-aop
  3. SymfonyCampUA-2012 video http://www.youtube.com/watch?v=ZXbREKT5GWE
  4. An example of interception of all methods in ZF2 (after cloning, we install dependencies via composer) https://github.com/lisachenko/zf2-aspect
  5. Interesting article on the topic: Aspects, filters, and signals - oh god! (en)

Also popular now: