Introducing aspect-oriented programming in PHP

  • Tutorial
When developing software, programmers and architects use decomposition - the representation of objects and the relationships between them in the form of classes, objects, their properties and methods.


Carrying out the decomposition, it is possible to obtain a more accurate representation of objects from real life in the form of program code. Thanks to this principle, object-oriented programming has gained such wide popularity in all programming languages. The model for representing real life objects as instances of classes is very convenient: we can endow a class with a set of methods and properties, allowing us to interact with the environment.

But is everything so convenient?


The OOP model allows you to answer the following questions: what or who (an object), what type (class), what it looks like (inheritance), what is common (abstraction), what can it do (methods), what attributes does it have (properties of an object or class) what functionality implements (interfaces). Here is a sample list of questions that we constantly answer in the form of our code. The answers to these questions cover almost all the properties and phenomena that occur with objects in real life.

Why almost everything?


In real life, there are a lot of nuances in the interaction of objects that are difficult to imagine in OOP: the sequence of interrelated actions of different objects, the temporary logic of phenomena, the need to perform additional actions when performing a specific action with the object. In life, this is described in the form of tips and recommendations: “wash your hands before eating”, “brush your teeth after eating”, “before leaving the house - turn off the light” and others. These actions are not easy to describe using methods: you need to use various decorators for classes, or explicitly introduce the interaction logic into the object method itself. In both cases, these rules cannot be conveniently formalized in the form of code using standard tools - and this leads to a complication of the system and closer binding of components.

How can I solve this problem?

   
A solution to this problem was invented a long time ago - to supplement the existing OOP model with some extension that will allow describing such interactions formally. A study was conducted by a group of Xerox PARC engineers, as a result of which they proposed a new paradigm - aspect-oriented programming. The essence of the idea is simple - to allow the program to look at itself “from the side” using the reflection mechanism and, if necessary, to change the final code. Having the ability to change the final code, AOP gets unlimited access to create hooks anywhere in the code and to extend this code with the help of tips.
 
In order to describe this behavior, the following terms were proposed:

1. Advice - advice. This is the action to be performed. For the statement "wash your hands before eating," the advice would be "wash your hands." As you can see, tips describe very real actions from the real world, which means that they can be represented in the form of methods, anonymous functions and closures. Each council knows which place it belongs to, so almost all information about the action is available in it.

2. Joinpoint - the point of implementation. This term defines a specific place in the program code where advice can be added. AspectJ, which is the primary source of AOP, has a large number of implementation points: access to a method, execute a method, initialize a class, create a new object, and access the property of an object. Go library! can work with calls to public and protected methods, both dynamic and static, including in final classes and traits; interception of protected and public properties at the object is supported. For example, the introduction point for “wash hands before eating” will be “the beginning of food”, or, in OOP terms, the “eat ()” method of the “human” class.

3. Pointcut - a slice of implementation points. A slice defines a plurality of implementation points at which to apply advice. In the world of AOP, this is an analogue of SELECT from SQL. The syntax for specifying a cutoff of points can be different, like the implementation itself, but as a rule, these are filters that receive many input points at the input, from which only suitable ones should be selected. In the library Go! annotations are mainly used for specifying slices, but in the future there will be support for xml, yaml and with the help of code. A slice of points may look like this: all public methods in a class, all static methods called * init (), all methods with a specific annotation, etc.

In addition to the signature itself, the slice also determines the relative place for the implementation of the council: before, after, around the point of implementation. For our case with hand washing, the cut of points will determine the only point of implementation: “before executing the method man-> eat ()”. If we want to achieve perfect cleanliness, we can determine the point of implementation “before performing the methods people -> * ()”, which will allow us to indicate our desire to always wash our hands before any action. However, an attentive reader may realize that the “man-> washHands ()” method also falls under this slice. What this threatens and how to avoid it will remain for independent study in order to stir up interest.

4. Aspect - the main unit in the framework of AOP, which allows you to put together slices of points with those tips that you need to apply. Since our case refers to a healthy lifestyle, we can call it HealthyLiveAspect and add our slice and some tips: wash your hands before eating and brush your teeth after eating. Let's make a list of what we have at the moment. We have a “human” class with clear methods, they do not have additional logic that would mix with the main code of the method. We have a separate aspect class with the necessary tips. It remains to do the smallest thing - to combine them into a single whole and get the finished code. This process is called weaving.

5. Weaving - the process of weaving code tips into the source code. This mechanism parses the source code using reflection and applies tips at the injection points. Go library! It uses the unique Load-Time Weaving technology for PHP, which allows you to track the time a class loads and change this code before it is parsed by the PHP parser. This makes it possible to dynamically change the class code, without changes in the source code by the developers. It all works as follows: at the beginning of the program, we initialize the AOP kernel, adding our aspects there, after which we transfer control to the main program. When a person creates an object, automatic loading of the class will work, which will determine the desired file name and try to download it. At this point, the call will be intercepted by the kernel, then static code analysis and verification of current aspects will be performed. The kernel will detect that there is a “person” class in the code and that tips need to be implemented in this class, so code transformers in the kernel will change the original class name, create a proxy class with the original class name and the overridden “eat” method, and pass a list of tips for given point. Further, the PHP parser parses this code and loads it into memory, while the decorator class will already be located by the name of the source class, so the usual call to the “eat” method will be wrapped at the connection point with the connected tips.

Better to see once than hear a hundred times.


Let's describe everything that we discussed using code. First of all, we will implement the class of a reasonable person who will be able to eat, sleep, work, and also wash hands and brush teeth. We will not do the __clone method yet :)

/**
 * Human class example
 */
class Human
{
    /**
     * Eat something
     */
    public function eat()
    {
        echo "Eating...", PHP_EOL;
    }
    /**
     * Clean the teeth
     */
    public function cleanTeeth()
    {
        echo "Cleaning teeth...", PHP_EOL;
    }
    /**
     * Washing up
     */
    public function washUp()
    {
        echo "Washing up...", PHP_EOL;
    }
    /**
     * Working
     */
    public function work()
    {
        echo "Working...", PHP_EOL;
    }
    /**
     * Go to sleep
     */
    public function sleep()
    {
        echo "Go to sleep...", PHP_EOL;
    }
}


At the moment, everything is written in Feng Shui: each method deals only with its own work, there is nothing superfluous in the methods. But we do not have the logic of washing our hands before eating, as well as brushing our teeth after eating and before going to bed:


/**
 * Human class example
 */
class Human
{
    /**
     * Eat something
     */
    public function eat()
    {
        $this->washUp();
        echo "Eating...", PHP_EOL;
        $this->cleanTeeth();
    }
    // ....
    /**
     * Go to sleep
     */
    public function sleep()
    {
        $this->cleanTeeth();
        echo "Go to sleep...", PHP_EOL;
    }
}


A typical programmer without much thought will do as described above: make a call to the necessary methods in the code of the “eat” and “sleep” methods, violating the principle of the sole responsibility of each of these methods. Fortunately, the conditions are simple, you can figure out why this was done here. In real life, everything is much sadder: how often do you come across a code that does different things in one place and there is no hint that this piece of code should be here? This is the famous metric: WTF / line of code. I think everyone has such examples in the code).

The next category of programmers feels the catch is that you need to change the logic of the method and they go to the other extreme: they make new methods in the class that combine the logic of several methods. Do you know the methods of the form “wash Hands and eat ()”?

On the one hand, the main methods will cease to contain this logic, but our class will begin to bloat, it will be possible to call the “eat ()” method directly, the class interface will be littered with unnecessary methods, and the number of errors in the code will begin to grow. Again not an option.

I already considered other options in my article on a hub on refactoring with AOP , therefore I will pass immediately to AOP.

Aspect Oriented Approach


You probably already understood that AOP allows you to divide into logical blocks what cannot be done using OOP. However, this technique is quite complicated, therefore AOP is intended primarily for those who have grasped all the intricacies of an object-oriented paradigm and want to discover something new.

The founder of aspect-oriented programming (AOP) Gregor Kikzalez shared in his interview his vision of the essence of AOP: "... AOP is essentially the next step in the development of structuring mechanisms. Today it is clear that objects do not replace procedures, but are only a way to create a higher level structuring mechanism. "And aspects, too, do not replace objects; they only provide another kind of structuring."

Let's try to describe our case from the perspective of an aspect-oriented paradigm. To do this, take the “pure” class of “man” with our “clean” methods and describe tips for the necessary methods using the aspect class: 

namespace Aspect;
use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;
/**
 * Healthy live aspect
 */
class HealthyLiveAspect implements Aspect
{
    /**
     * Pointcut for eat method
     *
     * @Pointcut("execution(public Human->eat(*))")
     */
    protected function humanEat() {}
    /**
     * Method that recommends to wash up before eating
     *
     * @param MethodInvocation $invocation Invocation
     * @Before(pointcut="humanEat()") // Short pointcut name
     */
    protected function washUpBeforeEat(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->washUp();
    }
    /**
     * Method that recommends to clean teeth after eating
     *
     * @param MethodInvocation $invocation Invocation
     * @After(pointcut="Aspect\HealthyLiveAspect->humanEat()") // Full-qualified pointcut name
     */
    protected function cleanTeethAfterEating(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }
    /**
     * Method that recommends to clean teeth before going to sleep
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Human->sleep())")
     */
    protected function cleanTeethBeforeSleep(MethodInvocation $invocation)
    {
        /** @var $person \Human */
        $person = $invocation->getThis();
        $person->cleanTeeth();
    }
}


I think this code is quite understandable in itself, but it is better to add some comments.

First, we defined a cut of points - an empty method humanEat(), marked with a special annotation @Pointcut("execution(public Human->eat(*))"). In principle, it was possible not to create a separate slice, but to indicate it before each tip separately, but since we have several tips for this slice, we can put it in a separate definition. The method itself and its code are not used in determining the slice and serve only to indicate and identify the slice in the aspect itself.

Secondly, we described the tips themselves in the form of aspect methods, indicating, with the help of annotations @Before, the @Afterspecific implementation site for the slice. You can immediately set slices in the annotation, as is done in the method cleanTeethBeforeSleep:@Before("execution(public Human->sleep())"). Each advice receives the necessary information about the execution point due to the object MethodInvocationcontaining the called object (class for the static method), arguments of the called method, as well as information about the point in the program (method reflection). Similar information can be obtained for accessing the properties of objects.

Now run our code:
include isset($_GET['original']) ? './autoload.php' : './autoload_aspect.php';
// Test case with human
$man = new Human();
echo "Want to eat something, let's have a breakfast!", PHP_EOL;
$man->eat();
echo "I should work to earn some money", PHP_EOL;
$man->work();
echo "It was a nice day, go to bed", PHP_EOL;
$man->sleep();


If we run this code in a browser, we can see the following output:

Want to eat something, let's have a breakfast!
Washing up...
Eating...
Cleaning teeth...
I should work to earn some money
Working...
It was a nice day, go to bed
Cleaning teeth...
Go to sleep...


For those interested in other files:
autoload.php
/**
 * Show all errors in code
 */
ini_set('display_errors', true);
/**
 * Register PSR-0 autoloader for our code, any components can be used here
 */
spl_autoload_register(function($originalClassName) {
    $className = ltrim($originalClassName, '\\');
    $fileName  = '';
    $namespace = '';
    if ($lastNsPos = strripos($className, '\\')) {
        $namespace = substr($className, 0, $lastNsPos);
        $className = substr($className, $lastNsPos + 1);
        $fileName  = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
    $resolvedFileName = stream_resolve_include_path($fileName);
    if ($resolvedFileName) {
        require_once $resolvedFileName;
    }
    return (bool) $resolvedFileName;
});
ob_start(function($content) {
    return str_replace(PHP_EOL, "
" . PHP_EOL, $content); });

autoload_aspect.php
include '../src/Go/Core/AspectKernel.php';
include 'DemoAspectKernel.php';
// Initialize demo aspect container
DemoAspectKernel::getInstance()->init(array(
    // Configuration for autoload namespaces
    'autoload' => array(
        'Go'               => realpath(__DIR__ . '/../src'),
        'TokenReflection'  => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'),
        'Doctrine\\Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/')
    ),
    // Default application directory
    'appDir' => __DIR__ . '/../demos',
    // Cache directory for Go! generated classes
    'cacheDir' => __DIR__ . '/cache',
    // Include paths for aspect weaving
    'includePaths' => array(),
    'debug' => true
));

DemoAspectKernel.php
use Aspect\HealthyLiveAspect;
use Go\Core\AspectKernel;
use Go\Core\AspectContainer;
/**
 * Demo Aspect Kernel class
 */
class DemoAspectKernel extends AspectKernel
{
    /**
     * Returns the path to the application autoloader file, typical autoload.php
     *
     * @return string
     */
    protected function getApplicationLoaderPath()
    {
        return __DIR__ . '/autoload.php';
    }
    /**
     * Configure an AspectContainer with advisors, aspects and pointcuts
     *
     * @param AspectContainer $container
     *
     * @return void
     */
    protected function configureAop(AspectContainer $container)
    {
        $container->registerAspect(new HealthyLiveAspect());
    }
}



What did we get? Firstly, the logic of the methods themselves in the “human” class does not contain any garbage. Already excellent, because it will be easier to maintain. Secondly, there are no methods that duplicate the main functionality of class methods. Thirdly, the logic is presented in the form of understandable tips in the aspect, because the tips have real meaning, which means that we just made a decomposition into functionality and made our application more structured! Moreover, the aspect class itself describes an understandable goal - a healthy lifestyle. Now everything is in its place and we can easily change both additional logic and work with the pure logic of the methods themselves.

I hope this simple example will help you better understand the concept of the aspect paradigm and try your hand at describing the aspect component of processes in the real world. For those interested, this example is available in the source code of the library in the demos / life.php folder, you can run it and study it)

Links:
  1. Official site http://go.aopphp.com
  2. Source code https://github.com/lisachenko/go-aop-php

Only registered users can participate in the survey. Please come in.

Are you interested in aspect-oriented programming?

  • 83.2% yes 483
  • 16.7% no 97

What do you think of AOP in PHP?

  • 35.2% An interesting paradigm, I want to know more about it to apply in a real project 218
  • 36.5% Interesting, but only theoretically, until I use 226 in my projects
  • 5.9% of AOP is already implicitly in the project, I use it very conveniently (Symfony2, FLOW, etc.) 37
  • 13.4% I will not use, since it is opaque and unusual for PHP 83
  • 8.7% I do not understand why it is necessary to use AOP at all and what it is 54

Also popular now: