Aspect-Oriented Programming in PHP with Go!

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.

Next, use Composer to install go-aop-php. Run the following command from a terminal:

Composer will install the required packages and dependencies in just a few seconds. If successful, you should see something similar to the following output:

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.

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.

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.

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.

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:

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.

That's all we need for the autoload.php file. Now, change BrokerTest.php to require the autoloader instead of Broker.php class.

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:

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:

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:

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 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:

Run the tests and check the output. You should see something similar to the following:

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.

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:

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:

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:

Let's play a little and offer a discount for a specific broker. Return to the test class, and write the following test:

This will fail with:

Next, we need to modify the broker to provide its ID. Just implement a getId() method, as demonstrated below:

Now, modify the aspect to adjust the buying price for a broker with an ID of 2.

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.

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:

Now, create an exception class. We need this because the built-in Exception class cannot be caught by Go! AOP or PHPUnit.

Modify the broker to throw an exception for a large value:

Run the tests and ensure that they fail:

Now, expect the exception (in the test) and make sure they pass:

Create a new method in our aspect to match @AfterThrowing, and don't forget to specify Use Go\Lang\Annotation\AfterThrowing;

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!

Tags:

Comments

Related Articles