Ray.AopはPHPでどのようにAOPを実現しているのか?
Ray.Aopを使うと、PHPでもAOP(アスペクト指向プログラミング)が使えます。
PHPの言語仕様にはAOP機能はありません。Ray.AopではどのようにAOPを実現しているのでしょう?
AOPとは?
例えば、Ray.Aopを使うと、以下のようにアノテーションを指定することで、そのメソッドをインターセプトし、メソッド実行の前後に処理を追加することができます。
src/HelloService.php
<?php
class HelloService
{
/**
* @Benchmark
*/
public function say()
{
return 'Hello World!';
}
}
例えば、以下のようなインターセプターを作成しバインドすれば、メソッド実行の時間を計測して表示できます。
src/Benchmarker.php
<?php
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
class Benchmarker implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
$start = microtime(true);
$result = $invocation->proceed(); // 元のメソッドの実行
$time = (microtime(true) - $start) * 1000;
$msg = sprintf("\n%s: %f ms", $invocation->getMethod()->getName(), $time);
return $result . $msg;
}
}
つまり、以下のようなコードを実行すると、
/* @var $hello HelloService */
echo $hello->say() . "\n";
通常は、以下になります。
Hello World!
しかし、Ray.Aopを使うと、以下のようになります。
Hello World!
say: 0.064135 ms
元のメソッドの実行結果の後に、ベンチマーク結果が追加されています。
HelloService::say()
は「Hello World!」を返すだけなのに不思議ですね。
以下の@Benchmark
アノテーションをHelloService
クラスから削除すれば、ベンチマークは表示されなくなります。
/**
* @Benchmark
*/
Ray.Aopの使い方
実際にコードを動作させるには、アノテーションを表すBenchmark
クラスも必要になります。
src/Benchmark.php
<?php
/**
* @Annotation
* @Target("METHOD")
*/
final class Benchmark
{
}
composer.json
は以下になります。
composer.json
{
"require": {
"ray/aop": "~2.0"
},
"autoload": {
"psr-4": {
"": "src/"
}
}
}
そして、クライアントコードは以下のようになります。
hello.php
<?php
require __DIR__ . '/vendor/autoload.php';
use Ray\Aop\Pointcut;
use Ray\Aop\Matcher;
use Ray\Aop\Bind;
use Ray\Aop\Compiler;
$pointcut = new Pointcut(
(new Matcher)->any(),
(new Matcher)->annotatedWith(Benchmark::class),
[new Benchmarker()]
);
$bind = (new Bind)->bind(HelloService::class, [$pointcut]);
$tmpDir = __DIR__ . '/tmp';
$compiler = (new Compiler($tmpDir));
$hello = $compiler->newInstance(HelloService::class, [], $bind);
/* @var $hello HelloService */
echo $hello->say() . "\n";
まず、ポイントカットを作成し、「すべてのクラス」の「@Benchmark
アノテーションのあるメソッド」に「Benchmarker
クラス」を指定します。
$pointcut = new Pointcut(
(new Matcher)->any(),
(new Matcher)->annotatedWith(Benchmark::class),
[new Benchmarker()]
);
次に、HelloService
クラスにそのポイントカットをバインドします。
$bind = (new Bind)->bind(HelloService::class, [$pointcut]);
最後にコンパイラを作成し、HelloService
クラスをインスタンス化します。
$tmpDir = __DIR__ . '/tmp';
$compiler = (new Compiler($tmpDir));
$hello = $compiler->newInstance(HelloService::class, [], $bind);
コンパイラには一時ディレクトリが必要なので用意しておく必要があります。
AOPの仕組み
コンパイラからインスタンスを作成するところに、AOPの仕組みがありそうですね。
一度コードを実行するとわかりますが、一時ディレクトリに以下のようなクラスファイルが作成されます。
tmp/HelloService_IzSUgmc.php
<?php
class HelloService_IzSUgmc extends HelloService implements Ray\Aop\WeavedInterface
{
private $isIntercepting = true;
public $bind;
/**
* @Benchmark
*/
function say()
{
if (isset($this->bindings[__FUNCTION__]) === false) {
return call_user_func_array('parent::' . __FUNCTION__, func_get_args());
}
if ($this->isIntercepting === false) {
$this->isIntercepting = true;
return call_user_func_array('parent::' . __FUNCTION__, func_get_args());
}
$this->isIntercepting = false;
$invocationResult = (new \Ray\Aop\ReflectiveMethodInvocation($this, new \ReflectionMethod($this, __FUNCTION__), new \Ray\Aop\Arguments(func_get_args()), $this->bindings[__FUNCTION__]))->proceed();
$this->isIntercepting = true;
return $invocationResult;
}
}
これがAOPの秘密です。
$hello
をダンプしてみると、以下のようになっています。
class HelloService_IzSUgmc#128 (3) {
private $isIntercepting =>
bool(true)
public $bind =>
NULL
public $bindings =>
array(1) {
'say' =>
array(1) {
[0] =>
class Benchmarker#3 (0) {
...
}
}
}
}
つまり、Ray.Aopではコンパイラがインスタンスの生成時に、ソースを変更したクラスを動的に作成し、その変更したクラスをインスタンス化し返します。
これで、メソッドのインターセプトが可能になります。
また、これが、READMEにある以下の制限の理由です。
クラスとメソッドは以下のものである必要があります。
- クラスは final ではない
- メソッドは public
- メソッドは final ではない
検証したバージョン
この記事は以下のバージョンで検証しました。
$ composer show -i
doctrine/annotations v1.2.7 Docblock Annotations Parser
doctrine/lexer v1.0.1 Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.
nikic/php-parser v1.4.1 A PHP parser written in PHP
ray/aop 2.1.1 An aspect oriented framework
見てわかるように、Ray.Aopではアノテーションの解析に「doctrine/annotations」、PHPコードの解析に「nikic/php-parser」を利用しています。
参考
Date: 2015/10/27