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.
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.
- Go! AOP PHP: Userland AOP implementation with quite some binding to e.g. Yii2, ZF2, Symfony and Laravel by Alexander Lisachenko.
- Exar Framework Another userland AOP implementation for PHP
- appserver.io The PHP appserver
appserver.io
also comes with support for AOP
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 theShopware\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 ourMonitorAspect
- Instantiation and configuration: The last step is creating an instance of the AOP kernel using
ApplicationAspectKernel::getInstance()
. Theinit
method allows quite some configuration, in our case especially thecacheDir
,appDir
,includePaths
andexcludePaths
are relevant. We register thecacheDir
to beaopcache
and will have a look at it later. TheappDir
is set to Shopware's engine directory, theengine/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