アスペクト指向プログラミング(AOP)は、PHPの新しい概念です。現在、PHP は AOP を正式にサポートしていませんが、この機能を実装する拡張機能やライブラリが多数あります。このレッスンでは、Go! PHP ライブラリを使用して、AOP 開発に PHP を使用する方法を学習します。必要に応じて、戻ってきて確認することもできます。
アスペクト指向プログラミングのアイデアは、1990 年代半ばにゼロックス パロアルト研究センター (PARC) で具体化されました。多くの興味深い新技術と同様、AOP も当初は明確な定義が欠如していたため物議を醸しました。そこでグループは、より広いコミュニティからフィードバックを得るために、未完成のアイデアを公開することにしました。重要な問題は「懸念の分離」という概念です。 AOP は、懸念事項を分離するための実行可能なシステム ソリューションです。
AOP は 1990 年代後半に成熟し、Xerox AspectJ のリリースによって特徴づけられ、続いて 2001 年に IBM が Hyper/J をリリースしました。現在、AOP は、一般的に使用されるプログラミング言語としては成熟したテクノロジーです。
AOPの中核は「アスペクト」ですが、「アスペクト」を定義する前に、「ポイントカット『ポイントカット』」と「通知『アドバイス』」という2つの用語について説明する必要があります。 」。ポイントカットはコード内の時点、具体的にはコードが実行されている時間を表します。ポイントカットでのコードの実行は通知と呼ばれ、1 つまたは複数のポイントカットと通知を組み合わせることが 1 つの側面 です。
通常、各クラスには中心となる行動や焦点がありますが、場合によっては、クラスに二次的な行動がある場合もあります。たとえば、クラスはロガーを呼び出したり、オブザーバーに通知したりする場合があります。クラス内のこれらの関数は二次的なものであるため、それらの動作は通常は同じです。この動作は「懸念事項の交差」と呼ばれます。AOP を使用すると回避できます。
PHP 用のさまざまな AOP ツール別のフレームワークは、異なるアプローチを採用し、PHP インタープリターのレベルでその魔法を主張する C/C++ で書かれた PHP 拡張機能を作成します。これは AOP PHP Extension と呼ばれるもので、これについては次の記事で説明します。
しかし、前にも言ったように、この記事では Go! AOP-PHP ライブラリをレビューします。
Go! をインストールして設定する
Composer を使用して Go! をインストールします
まず、composer.json ファイルに次の行を追加します。
{
"require" : {
"lisachenko/go-aop-php" : "*"
}
}
|
$ cd /your/project/folder
$ php composer.phar update lisachenko /go-aop-php
|
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
|
インストールが完了すると、コードディレクトリにvendorという名前のフォルダーが見つかります。 Go! ライブラリとその要件はここにインストールされます。
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
|
アプリケーションのルート/エントリポイント間に呼び出しを作成する必要があります。その後、オートローダー クラスが自動的に組み込まれます。アスペクトカーネルとしてのリファレンスを始めましょう。
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' ;
}
}
|
autoload.php については心配しないでください。それだけです。省略された部分を補っていきます。
初めてGo言語をインストールしたとき!そして、プロセスのこの時点に到達するには、いくつかのコードを実行する必要があると感じました。そこで、小さなアプリケーションの構築を開始します。
私たちの「アスペクト」は単純なロガーですが、アプリケーションの主要部分に進む前に確認すべきコードがいくつかあります。
私たちの小さなアプリケーションは、株式を売買できる電子ブローカーです。
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 ;
}
}
|
このコードは非常に単純です。Broker クラスには、ブローカーの名前と ID を格納する 2 つのプライベート フィールドがあります。
このクラスは、それぞれ株式を売買するための buy() と sell() という 2 つのメソッドも提供します。各メソッドは、株式 ID、株式数、および 1 株あたりの価格の 3 つのパラメーターを受け入れます。 sell() メソッドは株式を売却し、総収益を計算します。したがって、buy() メソッドは株式を購入し、総支払額を計算します。
PHPUnit テスト プログラムを通じて、ブローカーを簡単にテストできます。アプリケーション ディレクトリ内に Test という名前のサブディレクトリを作成し、そこに BrokerTest.php ファイルを追加します。次のコードを追加します:
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));
}
}
|
このチェッカーはブローカーメソッドの戻り値をチェックします。このチェッカーを実行して、コードが少なくとも構文的に正しいことを確認できます。
アプリケーションがクラスを必要とするときにクラスをロードするオートローダーを作成しましょう。これは、PSR-0 オートローダーをベースにしたシンプルなローダーです。
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 ;
});
|
これが autoload.php ファイルのすべての内容です。次に、BrokerTest.php を変更して、オートローダーを参照する Broker.php を参照します。
1 2 3 4 5 |
require_once '../autoload.php' ;
class BrokerTest extends PHPUnit_Framework_TestCase {
// [...]
}
|
BrokerTest を実行してコードの動作を確認します。
最後に Go! を設定する必要があります。これを行うには、すべてのコンポーネントが調和して動作するように接続する必要があります。まず、次のコードを使用して PHP ファイル AspectKernelLoader.php を作成します:
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)
|
このようにして、ブローカー上で何かが起こるたびにコードを実行させることができました。
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" ;
}
}
|
このメソッドは、パブリック メソッドが実行された後に実行されます (@After マッチャーに注意してください)。汚染するために、メソッドの呼び出しに使用されるパラメータを出力する別の行を追加します。テストでは次のように出力されます:
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)
|
これまで、メソッドの実行前後に追加のコードを実行する方法を学習しました。この優れた実装は実装されていますが、メソッドが何を返すかが分からない場合はあまり役に立ちません。アスペクトに別のメソッドを追加し、既存のコードを変更します。
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)
|
ちょっとした変更を加えて、特定のブローカーに割引を割り当てます。テスト クラスに戻り、次のテストを作成します:
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));
}
}
|
これは失敗します:
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.
|
次に、ID を提供するようにブローカーを変更する必要があります。以下に示すように agetId() メソッドを実装するだけです:
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;
}
// [...]
}
|
次に、ID 値 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 ;
}
}
|
新しいメソッドを追加する必要はなく、aroundMethodExecution()関数を変更するだけです。これで、「buy」というメソッドと一致し、$invocation->getThis() がトリガーされます。これにより、事実上、元の Broker オブジェクトが返され、そのコードを実行できるようになります。それで、やってみました!ブローカーに ID を要求し、ID が 2 の場合に割引を提供します。これでテストは成功しました。
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)
|
メソッドの開始と実行後、およびバイパス時に追加のプロシージャを実行できるようになりました。しかし、メソッドが例外をスローした場合はどうなるでしょうか?
Microsoft 株を大量に購入するためのテスト メソッドを追加します:
1 2 3 4 |
function testBuyTooMuch() {
$broker = new Broker( 'Finch' , '2' );
$broker ->buy( 'MS' , 10000, 8);
}
|
次に、例外クラスを作成します。組み込みの例外クラスは Go!AOP や PHPUnit でキャッチできないため、これが必要です。
1 2 3 4 5 6 7 |
class SpentTooMuchException extends Exception {
public function __construct( $message ) {
parent::__construct( $message );
}
}
|
大きな値に対して例外をスローするようにブローカー クラスを変更します。
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 ;
}
// [...]
}
|
テストを実行し、失敗メッセージが生成されることを確認します。
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.
|
次に、(テストで) 例外を予期し、例外が通過することを確認します。
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);
}
}
|
@AfterThrowing に一致する新しいメソッドを「アスペクト」に作成します。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(*))")
|