Advertisement
PHP

Aspect-Oriented Programming in PHP with Go!

by

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 a cache 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 to false 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, called Broker, 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!

Related Posts
  • Code
    Web Development
    Test Code Coverage: From Myth to RealityXdebug wide retina preview
    Learn the myths about test code coverage and how to use it to analyze how much of your production code has been tested.Read More…
  • Code
    Web Development
    Refactoring Legacy Code: Part 1 - The Golden MasterRefactoring wide retina preview
    Learn techniques for how to deal with complex and complicated unknown legacy code, how to understand it, and finally writing the Golden Master tests for future changes.Read More…
  • Code
    PHP
    Setting Up a Local Mirror for Composer Packages With SatisComposer retina preview
    Installing all your PHP libraries with Composer is a great way to save time. But larger projects automatically tested and run at each commit to your software version control (SVC) system will take a long time to install all the required packages from the Internet. You want to run your tests as soon as possible through your continuous integration (CI) system so that you have fast feedback and quick reactions on failure. In this tutorial we will set up a local mirror to proxy all your packages required in your project's composer.json file. This will make our CI work much faster, install the packages over the local network or even hosted on the same machine, and make sure we have the specific versions of the packages always available.Read More…
  • Code
    PHP
    Validation and Exception Handling: From the UI to the BackendProcedural to oop php retina preview
    Sooner or later in your programming career you will be faced with the dilemma of validation and exception handling. This was the case with me and my team also. A couple or so years ago we reached a point when we had to take architectural actions to accommodate all the exceptional cases our quite large software project needed to handle. Below is a list of practices we came to value and apply when it comes to validation and exception handling.Read More…
  • Code
    PHP
    The Repository Design PatternRepository pattern retina preview
    The Repository Design Pattern, defined by Eric Evens in his Domain Driven Design book, is one of the most useful and most widely applicable design patterns ever invented. Any application has to work with persistence and with some kind of list of items. These can be users, products, networks, disks, or whatever your application is about. If you have a blog for example, you have to deal with lists of blog posts and lists of comments. The problem that all of these list management logics have in common is how to connect business logic, factories and persistence.Read More…
  • Code
    PHP
    Taming Slim 2.0Slim
    Slim is a lightweight framework that packs a lot of punch for its tiny footprint. It has an incredible routing system, and offers a solid base to work from without getting in your way. Let me show you!Read More…