Aspect-oriented programming (AOP) is a new concept for PHP. Currently, PHP does not have official support for AOP, but there are many extensions and libraries that implement this feature. In this lesson, we will use the Go! PHP library to learn how to use PHP for AOP development, or you can come back and take a look if needed.
<p>Aspect-Oriented programming is like a new gadget for geeks.</p>
The idea of aspect-oriented programming took shape at the Xerox Palo Alto Research Center (PARC) in the mid-1990s. Like many interesting new technologies, AOP was initially controversial due to a lack of clear definition. So the group decided to make the unfinished idea public to receive feedback from the wider community. The key issue is the concept of "Separation of Concerns". AOP is a viable system solution for separation of concerns.
AOP matured in the late 1990s, marked by the release of Xerox AspectJ, followed by IBM with the release of Hyper/J in 2001. Today, AOP is a mature technology for commonly used programming languages.
The core of AOP is "aspect", but before we define "aspect", we need to discuss two terms; "point-cut『 point-cut』" and "Notification『advise』". A pointcut represents a point in time in our code, specifically a time when our code is running. Running code at a pointcut is called an advice. Combining one or multiple pointcuts and advice is an aspect .
Usually, each class will have a core behavior or focus, but sometimes, a class may have secondary behaviors. For example, a class might call a logger or notify an observer. Because these functions within a class are secondary, their behavior is usually the same. This behavior is called "Crossing Concerns"; it can be avoided using AOP.
Chris Peters has discussed the Flow framework for implementing AOP in PHP. The Lithium framework also provides implementation of AOP.
Another framework takes a different approach and creates a PHP extension written in C/C++ that asserts its magic at the level of the PHP interpreter. It's called AOP PHP Extension, and I'll discuss it in a follow-up article.
But as I said before, this article will review the Go! AOP-PHP library.
The Go! library is not extended; it is written entirely in PHP and is intended for use with PHP 5.4 or higher. As a pure PHP library, it is easy to deploy and can be easily installed even in restricted and shared hosting environments that do not allow you to compile and install your own PHP extensions.
Composer is the preferred method for installing PHP packages. If you haven't used Composer, you can download it from the Go! GitHub repository.
First, add the following lines to your composer.json file.
1 2 3 4 5 |
{
"require" : {
"lisachenko/go-aop-php" : "*"
}
}
|
After that, use Composer to install go-aop-php. Run the following command in the terminal:
1 2 |
$ cd /your/project/folder
$ php composer.phar update lisachenko /go-aop-php
|
Composer will install the referenced packages and requirements within a few seconds. If successful, you will see output similar to the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 is complete, you can find a folder named vendor in your code directory. The Go! library and its requirements are installed here.
1 2 3 4 5 6 7 8 9 10 11 |
$ 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
|
We need to create a call between the route/entry point of the application. The autoloader class is then included automatically. Let’s get started! Reference as an aspect kernel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use GoCoreAspectKernel;
use GoCoreAspectContainer;
class ApplicationAspectKernel extends AspectKernel {
protected function configureAop(AspectContainer $container ) {
}
protected function getApplicationLoaderPath() {
}
}
|
现在,AOP是一种在通用编程语言中相当成熟的技术。
例如,我创建了一个目录,调用应用程序,然后添加一个类文件: ApplicationAspectKernel.php 。
我们开始切面扩展!AcpectKernel 类提供了基础的方法用于完切面内核的工作。有两个方法,我们必须知道:configureAop()用于注册页面特征,和 getApplicationLoaderPath() 返回自动加载程序的全路径。
现在,一个简单的建立一个空的 autoload.php 文件在你的程序目录。和改变 getApplicationLoaderPath() 方法。如下:
1 2 3 4 5 6 7 8 9 10 |
// [...]
class ApplicationAspectKernel extends AspectKernel {
// [...]
protected function getApplicationLoaderPath() {
return __DIR__ . DIRECTORY_SEPARATOR . 'autoload.php' ;
}
}
|
Don’t worry about autoload.php and that’s it. We will fill in the omitted fragments.
When we installed the Go language for the first time! And to get to this point in my process, I felt the need to run some code. So start building a small application.
Our “aspect” is a simple logger, but there is some code to look at before moving on to the main part of our application.
Our little application is an electronic broker capable of buying and selling stocks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 ;
}
}
|
The code is very simple. The Broker class has two private fields that store the broker’s name and ID.
This class also provides two methods, buy() and sell(), which are used to purchase and sell stocks respectively. Each method accepts three parameters: stock ID, number of shares, and price per share. The sell() method sells the stock and calculates the total return. Accordingly, the buy() method buys the stock and calculates the total payout.
Through the PHPUnit test program, we can easily test our broker. Create a subdirectory within the application directory called Test and add the BrokerTest.php file there. and add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 checker checks the return value of the broker method. We can run this checker to verify that our code is at least syntactically correct.
Let’s create an autoloader that loads classes when the application needs them. This is a simple loader, based on PSR-0 autoloader.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 in our autoload.php file. Now, change BrokerTest.php to reference Broker.php to reference the autoloader.
1 2 3 4 5 |
require_once '../autoload.php' ;
class BrokerTest extends PHPUnit_Framework_TestCase {
// [...]
}
|
Run BrokerTest to verify the code operation.
The last thing we need to do is configure Go!. To do this, we need to connect all the components so that they work harmoniously. First, create a php file AspectKernelLoader.php with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
));
|
<p>我们需要连接所有的组件让们能和谐工作!</p>
这个文件位于前端控制器和自动加载器之间。他使用AOP框架初始化并在需要时调用autoload.php
第一行,我明确地载入AspectKernel.php和ApplicationAspectKernel.php,因为,要记住,在这个点我们还没有自动加载器。
接下来的代码段,我们调用ApplicationAspectKernel对象init()方法,并且给他传递了一个数列参数:
为了最后实现各个不同部分的连接,找出你工程中autoload.php自动加载所有的引用并且用AspectKernelLoader.php替换他们。在我们简单的例子中,仅仅test文件需要修改:
1 2 3 4 5 6 7 |
require_once '../AspectKernelLoader.php' ;
class BrokerTest extends PHPUnit_Framework_TestCase {
// [...]
}
|
对大一点的工程,你会发现使用bootstrap.php作为单元测试但是非常有用;用require_once()做为autoload.php,或者我们的AspectKernelLoader.php应该在那载入。
创建BrokerAspect.php文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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" ;
}
}
|
我们在程序开始指定一些有对AOP框架有用的语句。接着,我们创建了自己的方面类叫BrokerAspect,用它实现Aspect。接着,我们指定了我们aspect的匹配逻辑。
1 |
* @Before( "execution(public Broker->*(*))" )
|
1 |
[operation - execution/access]([method/attribute type - public / protected ] [ class ]->[method/attribute]([params])
|
请注意匹配机制不可否认有点笨拙。你在规则的每一部分仅可以使用一个星号‘*‘。例如public Broker->匹配一个叫做Broker的类;public Bro*->匹配以Bro开头的任何类;public *ker->匹配任何ker结尾的类。
<p>public *rok*->将匹配不到任何东西;你不能在同一个匹配中使用超过一个的星号。</p>
紧接着匹配程序的函数会在有时间发生时调用。在本例中的方法将会在每一个Broker公共方法调用之前执行。其参数$invocation(类型为MethodInvocation)子自动传递到我们的方法的。这个对象提供了多种方式获取调用方法的信息。在第一个例子中,我们使用他获取了方法的名字,并且输出。
仅仅定义一个切面是不够的;我们需要把它注册到AOP架构里。否则,它不会生效。编辑ApplicationAspectKernel.php同时在容器上的configureAop()方法里调用registerAspect():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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());
}
}
|
运行测试和检查输出。你会看到类似下面的东西:
1 2 3 4 5 6 7 8 9 |
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)
|
Just like that we have managed to make the code execute whenever something happens on the broker.
Let’s add another method to BrokerAspect.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// [...]
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()) . ".nn" ;
}
}
|
This method runs after a public method is executed (note the @After matcher). To taint we add another line to print out the parameters used to call the method. Our test now outputs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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)
|
So far, we have learned how to run additional code before and after a method is executed. While this nice implementation is implemented, it's not very useful if we can't see what the method returns. We add another method to the aspect and modify the existing code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//[...]
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() . "()nn" ;
}
/**
* @param MethodInvocation $invocation Invocation
* @Around("execution(public Broker->*(*))")
*/
public function aroundMethodExecution(MethodInvocation $invocation ) {
$returned = $invocation ->proceed();
echo "method returned: " . $returned . "\n" ;
return $returned ;
}
}
|
<p>仅仅定义一个aspect是不够的;我们需要将它注册到AOP基础设施。</p>
这个新的代码把参数信息移动到@Before方法。我们也增加了另一个特殊的@Around匹配器方法。这很整洁,因为原始的匹配方法调用被包裹于aroundMethodExecution()函数之内,有效的限制了原始的调用。在advise里,我们要调用$invocation->proceed(),以便执行原始的调用。如果你不这么做,原始的调用将不会发生。
这种包装也允许我们操作返回值。advise返回的就是原始调用返回的。在我们的案例中,我们没有修改任何东西,输出应该看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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)
|
We add a little change and assign a discount to a specific broker. Return to the test class and write the following test:
1 2 3 4 5 6 7 8 9 10 11 12 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
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 the agetId() method as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 purchase price of the broker with ID value 2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// [...]
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 ;
}
}
|
There is no need to add new methods, just modify the aroundMethodExecution() function. Now it matches the method, called 'buy', and triggers $invocation->getThis(). This effectively returns the original Broker object so that we can execute its code. So we did it! We ask the broker for its ID and provide a discount if the ID is equal to 2. The test now passes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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)
|
We can now execute additional procedures after the start and execution of a method, when bypassing. But what about when a method throws an exception?
Add a test method to buy a large amount of Microsoft stock:
1 2 3 4 |
function testBuyTooMuch() {
$broker = new Broker( 'Finch' , '2' );
$broker ->buy( 'MS' , 10000, 8);
}
|
Now, create an exception class. We need it because the built-in exception classes cannot be caught by Go!AOP or PHPUnit.
1 2 3 4 5 6 7 |
class SpentTooMuchException extends Exception {
public function __construct( $message ) {
parent::__construct( $message );
}
}
|
Modify the broker class and throw an exception for large values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 make sure they produce failure messages:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 exceptions (in tests) and make sure they pass:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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, don't forget to specify Use GoLangAnnotationAfterThrowing;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// [...]
Use GoLangAnnotationAfterThrowing;
class BrokerAspect implements Aspect {
// [...]
/**
* @param MethodInvocation $invocation Invocation
* @AfterThrowing("execution(public Broker->buy(*))")
|