The concept of Aspect-Oriented Programming (AOP) is fairly new to PHP. There's currently no official AOP support in PHP, but there are some extensions and libraries which implement this feature. In this lesson, we'll use the Go! PHP library to learn AOP in PHP, and review when it can be helpful.
A Brief History of AOP
Aspect-Oriented programming is like a new gadget for geeks.
The term Aspect-Oriented Programming took shape in the mid-1990s, inside a small group at Xerox Palo Alto Research Center (PARC). AOP was considered controversial in its early days — as is the case with any new and interesting technology — mostly due to its lack of clear definition. The group made the conscious decision to release it in a half-baked form, in order to let the larger community provide feedback. At the heart of the problem was the "Separation of Concerns" concept. AOP was one possible solution to separate concerns.
AOP matured in the late 1990s, when Xerox released AspectJ, and IBM followed suit with their Hyper/J in 2001. Today, AOP is a well-established technology that has been adopted by most common programming languages.
The Basic Vocabulary
At the heart of AOP is the aspect, but before we can define "aspect," we must discuss two other terms: point-cut and advise. A point-cut represents a moment in our source code, specifying the right moment to run our code. The code that executes at a point-cut is called, advise, and the combination of one or more point-cuts and advises is the aspect.
Typically, each class has one core behavior or concern, but in many situations, a class may exhibit secondary behavior. For example, a class may need to call a logger or notify an observer. Because these functionalities are secondary, their behavior is mostly the same for all the classes that exhibit them. This scenario is called a cross-concern; these can be avoided by using AOP.
The Various AOP Tools for PHP
Chris Peters already discussed the Flow framework for AOP in PHP. Another AOP implementation can be found in the Lithium framework.
Another framework took a different approach, and created a complete PHP extension in C/C++, performing its magic on the same level as the PHP interpreter. It is called the AOP PHP Extension, and I may discuss it in a future article.
But as I noted earlier, for this tutorial, we'll review the Go! AOP-PHP library.
Installing and Preparing Go!
The Go! library is not an extension; it's completely written in PHP for PHP 5.4 and higher. Being just a plain PHP library allows for easy deployment, even in restrictive, shared-hosting environments that do not allow you to compile and install your own PHP extensions.
Install Go! with Composer
Composer is the preferred method for installing PHP packages. If you do not have access to Composer, you can always download it from the Go! GitHub repository.
First, add the following lines to your composer.json
file.
{ "require": { "lisachenko/go-aop-php": "*" } }
Next, use Composer to install go-aop-php
. Run the following command from a terminal:
$ cd /your/project/folder $ php composer.phar update lisachenko/go-aop-php
Composer will install the required packages and dependencies in just a few seconds. If successful, you should see something similar to the following output:
Loading composer repositories with package information Updating dependencies - Installing doctrine/common (2.3.0) Downloading: 100% - Installing andrewsville/php-token-reflection (1.3.1) Downloading: 100% - Installing lisachenko/go-aop-php (0.1.1) Downloading: 100% Writing lock file Generating autoload files
After the installation has completed, you will find a directory, called vendor
, in your source
folder. The Go! library and its dependencies are installed there.
$ ls -l ./vendor total 20 drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 andrewsville -rw-r--r-- 1 csaba csaba 182 Feb 2 12:18 autoload.php drwxr-xr-x 2 csaba csaba 4096 Feb 2 12:16 composer drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 doctrine drwxr-xr-x 3 csaba csaba 4096 Feb 2 12:16 lisachenko $ ls -l ./vendor/lisachenko/ total 4 drwxr-xr-x 5 csaba csaba 4096 Feb 2 12:16 go-aop-php
Integrate Go! Into Our Project
We need to create a call that sits between the routing/entry point of our application. The autoloader then automatically includes the class. Go! refers to this as an Aspect Kernel.
use Go\Core\AspectKernel; use Go\Core\AspectContainer; class ApplicationAspectKernel extends AspectKernel { protected function configureAop(AspectContainer $container) { } protected function getApplicationLoaderPath() { } }
Today, AOP is a well-established technology that has been adopted by most common programming languages.
For this example, I've created a directory, called Application
, and added a new class file, ApplicationAspectKernel.php
within it.
Our aspect kernel extends Go!'s abstract AspectKernel
class, which provides the basic functionality that the aspect kernel requires to do its job. There are two methods we must implement: configureAop()
, which registers our future aspects, and getApplicationLoaderPath()
, which provides a string representing the full path to the application's autoloader.
For now, simply create an empty autoload.php
file in your Application
directory, and change the getApplicationLoaderPath()
method, accordingly.
// [...] class ApplicationAspectKernel extends AspectKernel { // [...] protected function getApplicationLoaderPath() { return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php'; } }
Don't worry about autoload.php
just yet; we'll fill in the missing pieces shortly.
When I first installed Go! and reached this point of my process, I felt the need to run some code. So let's begin building a small application!
Creating A Simple Logger
Our aspect will be a simple logger, but we first need some code to watch before we start with the main portion of our application.
Create A Minimal Application
Our small application will be an electronic broker, capable of buying and selling shares.
class Broker { private $name; private $id; function __construct($name, $id) { $this->name = $name; $this->id = $id; } function buy($symbol, $volume, $price) { return $volume * $price; } function sell($symbol, $volume, $price) { return $volume * $price; } }
This code is quite simple. The Broker
class has two private fields that store the broker's name and ID.
This class also offers two methods, buy()
and sell()
for buying and selling shares, respectively. Each of these methods accepts three arguments: the share's symbol, the number of shares, and the price per share. The sell()
method sells the shares and then calculates the total money received. Conversely, the buy()
method buys the shares and calculates the total money spent.
Exercise Our Broker
We can easily exercise our Broker
by writing a PHPUnit test. Create a directory, called Test
inside the Application
directory, and, within it, add a new file, BrokerTest.php
. Append the following code to that file:
require_once '../Broker.php'; class BrokerTest extends PHPUnit_Framework_TestCase { function testBrokerCanBuyShares() { $broker = new Broker('John', '1'); $this->assertEquals(500, $broker->buy('GOOGL', 100, 5)); } function testBrokerCanSellShares() { $broker = new Broker('John', '1'); $this->assertEquals(500, $broker->sell('YAHOO', 50, 10)); } }
This test simply checks the return values of the broker's methods. We can run this test and see that our code is at least syntactically correct.
Add an Auto Loader
Let's create an autoloader that physically loads the classes that our application needs. This will be a simple loader, based on the PSR-0 autoloader.
ini_set('display_errors', true); 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; });
That's all we need for the autoload.php
file. Now, change BrokerTest.php
to require the autoloader instead of Broker.php
class.
require_once '../autoload.php'; class BrokerTest extends PHPUnit_Framework_TestCase { // [...] }
Running BrokerTest
proves that the code still works.
Connecting to Application Aspect Kernel
Our final step is to configure Go!. We need to connect all the components so that they work in harmony. First, create a file, called AspectKernelLoader.php
, and add the following code:
include __DIR__ . '/../vendor/lisachenko/go-aop-php/src/Go/Core/AspectKernel.php'; include 'ApplicationAspectKernel.php'; ApplicationAspectKernel::getInstance()->init(array( 'autoload' => array( 'Go' => realpath(__DIR__ . '/../vendor/lisachenko/go-aop-php/src/'), 'TokenReflection' => realpath(__DIR__ . '/../vendor/andrewsville/php-token-reflection/'), 'Doctrine\\Common' => realpath(__DIR__ . '/../vendor/doctrine/common/lib/') ), 'appDir' => __DIR__ . '/../Application', 'cacheDir' => null, 'includePaths' => array(), 'debug' => true ));
We need to connect all the components so that they work in harmony.
This file sits between the front controller and the autoloader. It uses the AOP infrastructure to initialize and call the autoload.php
when needed.
In the first line, we explicitly include AspectKernel.php
and ApplicationAspectKernel.php
. These files must be explicitly included, because, remember, we have no autoloader at this point.
In the following code segment, we call the init()
method on the ApplicationAspectKernel
object and pass it an array of options:
-
autoload
defines the paths to initialize for the AOP library. Adjust the paths according to your directory structure. -
appDir
refers to the application's directory. -
cacheDir
specifies acache
directory (we'll ignore this for this tutorial). -
includePaths
represents a filter for the aspects. We want all the specified directories to be watched, so leave this array empty to watch everything. -
debug
provides extra debugging information, which is useful for development, but you should set it tofalse
for deployed applications.
To finalize the connection between the different pieces, find all references to autoload.php
in your project and replace them with AspectKernelLoader.php
. In our simple example, only the test file requires modification:
require_once '../AspectKernelLoader.php'; class BrokerTest extends PHPUnit_Framework_TestCase { // [...] }
For bigger projects, you may find that it's useful to use bootstrap.php
for PHPUnit; the require_once()
for autoload.php
or our AspectKernelLoader.php
should be included there.
Log the Broker's Methods
Create a file, called BrokerAspect.php
, and add the following code:
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; use Go\Lang\Annotation\DeclareParents; class BrokerAspect implements Aspect { /** * @param MethodInvocation $invocation Invocation * @Before("execution(public Broker->*(*))") // This is our PointCut */ public function beforeMethodExecution(MethodInvocation $invocation) { echo "Entering method " . $invocation->getMethod()->getName() . "()\n"; } }
We begin by specifying several use
statements for the AOP infrastructure. Then, we create our aspect class, called BrokerAspect
, which must implement Aspect
. Next, we specify the matching logic for our aspect:
* @Before("execution(public Broker->*(*))")
-
@Before
specifies when to apply the advice. Possibilities are@Before
,@After
,@Around
and@AfterThrowing
. -
"execution(public Broker->*(*))"
specifies the matching rule as the execution of any public method on a class, calledBroker
, with any number of parameters. The syntax is:[operation - execution/access]([method/attribute type - public/protected] [class]->[method/attribute]([params])
Please note that the matching mechanism is admittedly somewhat awkward. You can use only one '*
' (star) in each part of the rule. For example, public Broker->
matches a class, called Broker
. public Bro*->
matches any class starting with Bro
, and public *ker->
matches any class ending with ker
.
public *rok*->
will not match anything; you may not use more than one star for the same match.
The method following the matcher will be called when the event occurs. In our case, the method executes before each call on one of Broker
's public methods. A parameter, called $invocation
(of type MethodInvocation
), is automatically passed to our method. This object provides different ways of obtaining information about the called method. In this first example, we use it to obtain the method's name and print it.
Registering The Aspect
Merely defining an aspect is not enough; we need to register it into the AOP infrastructure. Otherwise, it will not be applied. Edit ApplicationAspectKernel.php
and call registerAspect()
on the container in the configureAop()
method:
use Go\Core\AspectKernel; use Go\Core\AspectContainer; class ApplicationAspectKernel extends AspectKernel { protected function getApplicationLoaderPath() { return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php'; } protected function configureAop(AspectContainer $container) { $container->registerAspect(new BrokerAspect()); } }
Run the tests and check the output. You should see something similar to the following:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() Entering method buy() .Entering method __construct() Entering method sell() Time: 0 seconds, Memory: 5.50Mb OK (2 tests, 2 assertions)
So we've managed to run code whenever something happens on the broker.
Finding Parameters and Matching @After
Let's add another method to the BrokerAspect
.
// [...] class BrokerAspect implements Aspect { // [...] /** * @param MethodInvocation $invocation Invocation * @After("execution(public Broker->*(*))") */ public function afterMethodExecution(MethodInvocation $invocation) { echo "Finished executing method " . $invocation->getMethod()->getName() . "()\n"; echo "with parameters: " . implode(', ', $invocation->getArguments()) . ".\n\n"; } }
This method runs after a public method executes (note the @After
matcher). We then add another line to output the parameters used to call the method. The output of our test now is:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() Finished executing method __construct() with parameters: John, 1. Entering method buy() Finished executing method buy() with parameters: GOOGL, 100, 5. .Entering method __construct() Finished executing method __construct() with parameters: John, 1. Entering method sell() Finished executing method sell() with parameters: YAHOO, 50, 10. Time: 0 seconds, Memory: 5.50Mb OK (2 tests, 2 assertions)
Getting Return Values and Manipulating Execution
So far, we've learned how to run extra code before and after a method executes. While this is nice, it's not overly useful if we can't see what the methods return. Let's add another method to the aspect and modify the existing code:
//[...] class BrokerAspect implements Aspect { /** * @param MethodInvocation $invocation Invocation * @Before("execution(public Broker->*(*))") */ public function beforeMethodExecution(MethodInvocation $invocation) { echo "Entering method " . $invocation->getMethod()->getName() . "()\n"; echo "with parameters: " . implode(', ', $invocation->getArguments()) . ".\n"; } /** * @param MethodInvocation $invocation Invocation * @After("execution(public Broker->*(*))") */ public function afterMethodExecution(MethodInvocation $invocation) { echo "Finished executing method " . $invocation->getMethod()->getName() . "()\n\n"; } /** * @param MethodInvocation $invocation Invocation * @Around("execution(public Broker->*(*))") */ public function aroundMethodExecution(MethodInvocation $invocation) { $returned = $invocation->proceed(); echo "method returned: " . $returned . "\n"; return $returned; } }
Merely defining an aspect is not enough; we need to register it into the AOP infrastructure.
This new code moves the parameter information to the @Before
method. We also add another method with the special @Around
matcher. This is neat, because the original matched method call is wrapped inside the aroundMethodExecution()
function, effectively supressing the original invocation. Inside the advise, we need to call $invocation->proceed()
, in order to execute the original call. If you don't do this, the original call will not occur.
This wrapping also allows us to manipulate the returned value. What we return in our advise is what returns in the original call. In our case, we didn't change anything, and your output should look like this:
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() with parameters: John, 1. method returned: Finished executing method __construct() Entering method buy() with parameters: GOOGL, 100, 5. method returned: 500 Finished executing method buy() .Entering method __construct() with parameters: John, 1. method returned: Finished executing method __construct() Entering method sell() with parameters: YAHOO, 50, 10. method returned: 500 Finished executing method sell() Time: 0 seconds, Memory: 5.75Mb OK (2 tests, 2 assertions)
Let's play a little and offer a discount for a specific broker. Return to the test class, and write the following test:
require_once '../AspectKernelLoader.php'; class BrokerTest extends PHPUnit_Framework_TestCase { // [...] function testBrokerWithId2WillHaveADiscountOnBuyingShares() { $broker = new Broker('Finch', '2'); $this->assertEquals(80, $broker->buy('MS', 10, 10)); } }
This will fail with:
Time: 0 seconds, Memory: 6.00Mb There was 1 failure: 1) BrokerTest::testBrokerWithId2WillHaveADiscountOnBuyingShares Failed asserting that 100 matches expected 80. /home/csaba/Personal/Programming/NetTuts/Aspect Oriented Programming in PHP/Source/Application/Test/BrokerTest.php:19 /usr/bin/phpunit:46 FAILURES! Tests: 3, Assertions: 3, Failures: 1.
Next, we need to modify the broker to provide its ID. Just implement a getId()
method, as demonstrated below:
class Broker { private $name; private $id; function __construct($name, $id) { $this->name = $name; $this->id = $id; } function getId() { return $this->id; } // [...] }
Now, modify the aspect to adjust the buying price for a broker with an ID of 2
.
// [...] class BrokerAspect implements Aspect { // [...] /** * @param MethodInvocation $invocation Invocation * @Around("execution(public Broker->buy(*))") */ public function aroundMethodExecution(MethodInvocation $invocation) { $returned = $invocation->proceed(); $broker = $invocation->getThis(); if ($broker->getId() == 2) return $returned * 0.80; return $returned; } }
Instead of adding a new method, just modify the aroundMethodExecution()
function. It now only matches methods, called 'buy
', and triggers $invocation->getThis()
. This effectively returns the original Broker
object so that we may execute its code. And so we did! We ask the broker for its ID, and offer a discount if the ID is equal to 2
. The test now passes.
PHPUnit 3.6.11 by Sebastian Bergmann. .Entering method __construct() with parameters: John, 1. Finished executing method __construct() Entering method buy() with parameters: GOOGL, 100, 5. Entering method getId() with parameters: . Finished executing method getId() Finished executing method buy() .Entering method __construct() with parameters: John, 1. Finished executing method __construct() Entering method sell() with parameters: YAHOO, 50, 10. Finished executing method sell() .Entering method __construct() with parameters: Finch, 2. Finished executing method __construct() Entering method buy() with parameters: MS, 10, 10. Entering method getId() with parameters: . Finished executing method getId() Finished executing method buy() Time: 0 seconds, Memory: 5.75Mb OK (3 tests, 3 assertions)
Matching Exceptions
We can now execute extra code when a method is entered, after it executes and around it. But what if a method throws an exception?
Add a test method to buy a large number of shares:
function testBuyTooMuch() { $broker = new Broker('Finch', '2'); $broker->buy('MS', 10000, 8); }
Now, create an exception class. We need this because the built-in Exception
class cannot be caught by Go! AOP or PHPUnit.
class SpentTooMuchException extends Exception { public function __construct($message) { parent::__construct($message); } }
Modify the broker to throw an exception for a large value:
class Broker { // [...] function buy($symbol, $volume, $price) { $value = $volume * $price; if ($value > 1000) throw new SpentTooMuchException(sprintf('You are not allowed to spend that much (%s)', $value)); return $value; } // [...] }
Run the tests and ensure that they fail:
Time: 0 seconds, Memory: 6.00Mb There was 1 error: 1) BrokerTest::testBuyTooMuch Exception: You are not allowed to spend that much (80000) /home/csaba/Personal/Programming/NetTuts/Aspect Oriented Programming in PHP/Source/Application/Broker.php:20 // [...] /home/csaba/Personal/Programming/NetTuts/Aspect Oriented Programming in PHP/Source/Application/Broker.php:47 /home/csaba/Personal/Programming/NetTuts/Aspect Oriented Programming in PHP/Source/Application/Test/BrokerTest.php:24 /usr/bin/phpunit:46 FAILURES! Tests: 4, Assertions: 3, Errors: 1.
Now, expect the exception (in the test) and make sure they pass:
class BrokerTest extends PHPUnit_Framework_TestCase { // [...] /** * @expectedException SpentTooMuchException */ function testBuyTooMuch() { $broker = new Broker('Finch', '2'); $broker->buy('MS', 10000, 8); } }
Create a new method in our aspect to match @AfterThrowing
, and don't forget to specify Use Go\Lang\Annotation\AfterThrowing;
// [...] Use Go\Lang\Annotation\AfterThrowing; class BrokerAspect implements Aspect { // [...] /** * @param MethodInvocation $invocation Invocation * @AfterThrowing("execution(public Broker->buy(*))") */ public function afterExceptionMethodExecution(MethodInvocation $invocation) { echo 'An exception has happened'; } }
The @AfterThrowing
matcher suppresses the thrown exception and allows you to take your own action. In our code, we simply echo a message, but you may do whatever your application requires.
Final Thoughts
This is why I urge you to use aspects with care.
Aspect oriented programming is like a new gadget for geeks; you can immediately see its great potential. Aspects allows us to introduce extra code in different parts of our system without modifying the original code. This can prove to be immensely helpful, when you need to implement modules that pollute your methods and classes with tightly coupled references and method calls.
This flexibility, however, comes with a price: obscurity. There is no way to tell if an aspect watches a method just by looking at the method or its class. For example, there is no indication in our Broker
class that anything happens when its methods execute. This is why I urge you to use aspects with care.
Our use of the aspect to offer a discount for a specific broker is an example of misuse. Refrain from doing that in a real project. A broker discount is related to brokers; so, keep that logic in the Broker
class. Aspects should only perform tasks that do not directly relate to the object's primary behavior.
Have fun with this!
Comments