AOP

In the last blog post I discussed cross cutting concerns and ways to address them in PHP. In this post I want to have a look at AOP and try to integrate the GO AOP PHP library in Shopware.

Monitor aspect

AOP

AOP (aspect-oriented programming) is a programming paradigm which is usually attributed to Gregor Kiczales and his colleagues. The whole concept is about addressing cross-cutting concerns properly - not by choosing design patterns, but by having language constructs, which allow adding behaviour to parts of the program.

AOP terms

Speaking of AOP there are usually some special terms to know:

  • Advice: An advice is some code you want to add to your existing code base, e.g. to a certain model or service.
  • Pointcut: A pointcut is a point in your program, you want to connect your advice to. This could be the beginning or end of a function / method.
  • Aspect: A certain advice at a certain pointcut is considered an aspect. It is the concrete extension you do, in order to add e.g. logging to parts of your application.

AOP and PHP

As AOP is more a programming paradigm than a design pattern, the language should offer some kind of support for it. Gregor Kiczales, as one of the authors of the AOP approach, is also one of the authors of AspectJ, which is the most recognized AOP framework for Java.

PHP does not support AOP by default, for that reason AOP is not used often in real-world projects. There are some libraries and ways, however, to make use of AOP in PHP projects.

AOP as a userland implementation (thus: not changing the actual PHP core or adding a PHP extension) will usually be archived using a proxy pattern. So in order to add arbitrary code to your existing code base, the AOP framework will need to create proxy classes for your original classes - and will provide generic join points at this level. In your application you will then not work with your original classes, but with the proxy objects, which are transparent to the original class. This is pretty much, how the Shopware hook system works.

Go! AOP PHP and Shopware

In order to provide a real word example, we will try to make use of the Go! AOP PHP framework within Shopware. Of course this should be considered a proof of concept, neither should this be used in production nor there is any intend to include such an AOP framework in Shopware in the future.

Setting it up

First of all we set up a fresh Shopware 5.1 from Github using the CLI tools:

sw install:vcs -b5.1 -daop -iaop -paop

This will create Shopware inside a directory named aop with a new database also called aop. After a quick test, that Shopware has been set up properly, we will add the Go! AOP PHP library dependency to Shopware:

composer require goaop/framework

Introducing aspects to Shopware

In order to run the AOP framework, we need to register it early in Shopware's Bootstrap process. In fact, we need to bypass Shopware's autoloading, so we need to add it to the shopware.php before the actual Kernel is instantiated:

[…]
require __DIR__ . '/autoload.php';

use Shopware\Kernel;
use Shopware\Components\HttpCache\AppCache;
use Symfony\Component\HttpFoundation\Request;

// include our custom aop.php file
require_once 'aop.php';

$environment = getenv('SHOPWARE_ENV') ?: getenv('REDIRECT_SHOPWARE_ENV') ?: 'production';

$kernel = new Kernel($environment, $environment !== 'production');
if ($kernel->isHttpCacheEnabled()) {
    $kernel = new AppCache($kernel, $kernel->getHttpCacheConfig());
}

$request = Request::createFromGlobals();

$kernel->handle($request)
       ->send();

From now on we can leave the shopware.php alone and add all AOP related stuff to the aop.php file. A simple example could look like this:

<?php

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;


use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
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;

/**
 * Monitor aspect
 */
class MonitorAspect implements Aspect
{

    /**
     * Method that will be called before real method
     *
     * @param MethodInvocation $invocation Invocation
     * @Before("execution(public Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch->*(*))")
     */
    public function beforeMethodExecution(MethodInvocation $invocation)
    {
        error_log('Calling Before Interceptor for method: ' . $invocation->getMethod()->getName() . '()');
    }
}

/**
 * Application Aspect Kernel
 */
class ApplicationAspectKernel extends \Go\Core\AspectKernel
{

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

// Initialize an application aspect container
$applicationAspectKernel = ApplicationAspectKernel::getInstance();
$applicationAspectKernel->init(array(
        'debug' => true, // use 'false' for production mode
        // Cache directory
        'cacheDir' => __DIR__ . '/aopcache',
        'appDir' => __DIR__ . '/engine/',
        // Include paths restricts the directories where aspects should be applied, or empty for all source files
        'includePaths' => array(
            __DIR__ . '/engine/'
        ),
        'excludePaths' => array(
            __DIR__ . '/engine/Library'
        )
    )
);

As you can see, we basically have three parts here:

  • MonitorAspect: This is our example aspect, which will basically "hook" to all public function calls of the Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch of Shopware. The pointcut is defined with the @Before annotation the AOP framework provides (Go! AOP PHP uses doctrine annotations for this). Some more example can be found in the Go! AOP PHP documentation.
  • ApplicationAspectKernel: This is our custom AOP kernel, which extends from \Go\Core\AspectKernel. It is used to register the aspects of our application - in this case only our MonitorAspect
  • Instantiation and configuration: The last step is creating an instance of the AOP kernel using ApplicationAspectKernel::getInstance(). The init method allows quite some configuration, in our case especially the cacheDir, appDir, includePaths and excludePaths are relevant. We register the cacheDir to be aopcache and will have a look at it later. The appDir is set to Shopware's engine directory, the engine/Library directory is excluded, however, as there were issues regarding the Smarty template engine in my tests and we only want to extend the actual Shopware core anyway.

First run

After saving all changes, a call to http://localhost/aop will not show much of a change. But the AOP framework already populated the aopcache directory we specified before. There are (at least) the subdirectories:

_annotations

The cached and serialized doctrine annotations, so the AOP framework wouldn't need to parse them again

_proxies

The AOP proxies. As we only registered one aspect to the ProductNumberSearch, there is only one proxy aopcache/_proxies/Shopware/Bundle/SearchBundleDBAL/ProductNumberSearch.php. It looks like this:

class ProductNumberSearch extends ProductNumberSearch__AopProxied implements \Go\Aop\Proxy
{

    private static $__joinPoints = array();

    public function __construct(\Shopware\Bundle\SearchBundleDBAL\QueryBuilderFactory $queryBuilderFactory, \Shopware\Bundle\StoreFrontBundle\Gateway\DBAL\Hydrator\AttributeHydrator $attributeHydrator, \Enlight_Event_EventManager $eventManager, $facetHandlers = []
        )
    {
        return self::$__joinPoints['method:__construct']->__invoke($this, array($queryBuilderFactory, $attributeHydrator, $eventManager, $facetHandlers));
    }

    public function search(\Shopware\Bundle\SearchBundle\Criteria $criteria, \Shopware\Bundle\StoreFrontBundle\Struct\ShopContextInterface $context)
    {
        return self::$__joinPoints['method:search']->__invoke($this, array($criteria, $context));
    }

}
\Go\Proxy\ClassProxy::injectJoinPoints('Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch',array (
  'method' =>
  array (
    '__construct' =>
    array (
      0 => 'advisor.MonitorAspect->beforeMethodExecution',
    ),
    'search' =>
    array (
      0 => 'advisor.MonitorAspect->beforeMethodExecution',
    ),
  ),
));

Generally speaking it extends from the class ProductNumberSearch__AopProxied and implements the \Go\Aop\Proxy interface. The static call to injectJoinPoints at the end of the file will populate the internal jointPoints collection of the Proxy, the proxy itself, overwrites the __construct as well as the search method of the original class with calls like this:

return self::$__joinPoints['method:search']->__invoke($this, array($criteria, $context));

Just as the Shopware hook system this will call the registered aspects and also run the original code of the proxied class.

Shopware

In this directory the AOP framework will put proxied files, that were used during the test request. As you can see, not only the actually extended class, but also every other used class used during the request can be found here. But while all the files are basically copies of the corresponding original file, the class \Shopware\Bundle\SearchBundleDBAL\ProductNumberSearch__AopProxied now has a __AopProxied suffix. The rest of the file has the original content - and it is extend by the proxy shown above.

Showing some results

Until now we couldn't see any output, as it was written to error_log by the MonitorAspect. A simple tail will allow us to inspect the output:

tail -f /var/log/apache2/error.log

The output will look like this:

[Mon Aug 03 10:33:50.561880 2015] [:error] [pid 6753] [client ::1:43388] Calling Before Interceptor for method: __construct(), referer: http://localhost/aop/

Currently only the __construct method seems to be intercepted - basically because the ProductNumberSearch::search call is not called on the front page in my example. Changing to a category listing will call the method and print out something like:

[Mon Aug 03 10:36:34.958412 2015] [:error] [pid 15005] [client ::1:43453] Calling Before Interceptor for method: __construct(), referer: http://localhost/aop/
[Mon Aug 03 10:36:34.964612 2015] [:error] [pid 15005] [client ::1:43453] Calling Before Interceptor for method: search(), referer: http://localhost/aop/

Currently the MonitorAspect only prints out the called method name, looking at the base interface \Go\Aop\Intercept\MethodInvocation and \Go\Aop\Intercept\Invocation will show you, that we could also access the original method's argument using $invocation->getArguments(), which is also shown in the example section of the AOP framework.

Problems

As discussed in the Shopware hook system blog post, AOP tends to hide the actually executed logic: Looking at a certain service, you are not able to tell, what is actually being executed there, as there could be some aspects in the program, which add additional behaviour. The program might become less obvious and harder to debug.

There is another downside of the AOP-approach which is similar to the hook system of Shopware: Both can be very sensitive to changes of a program without giving the developer an immediate feedback about possible problems: From looking at the aspect alone, a developer is not able to tell, if the pointcut is still available / valid (at least in the GO AOP implementation we discussed above).

Furthermore there is a limitation, that derives from the used proxy pattern: Due to the inheritance used for it, only protected and public methods can be used as pointcuts, so it is not possible to use the same mechanism as shown above for private methods.

Further reading

  • Shopware AOP Github repo of the aop shopware branch
  • Discussing aspects of AOP Summary of AOP and cross cutting concerns in an interview with Gregor Kiczales and others
  • Strict PHP Strict PHP type handling; library by Marco Pivetta and Jefersson Nathan
Back to overview