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

Tags: php, aop