こんにちは。呼ばれて飛び出てジャジャジャジャーン、@madapajaです。
PHP5.4+フレームワーク BEAR.Sundayを理解するためにRay.Diを触ってみるの巻シリーズ
続きです。 前回は、Ray.Di を使って依存性の注入を行ってみました。 今回は、Ray.Aop でインターセプターを使い、アスペクト指向プログラミングの体験を中心に進めてみたいと思います。
前回の補足
…と、その前に前回の補足を。
@PostConstruct
アノテーション
前回、@PostConstruct
アノテーションによって初期化メソッド(__construct()
後に実行するメソッド)の定義を行いました。
これはコンストラクタでやるのと、何が違うのでしょうか? 今回のサンプルの場合、コンストラクタ上で呼び出しても挙動は変わりません。
なぜコンストラクタで行わないのか、その理由を3つほど考えてみました。(@koriymさんからもアドバイスをもらいました)
1つめは、関心を分離させるためです。
コンストラクタでは文字通り構造を構成する(つまり、プロパティへの代入)のみに徹し、@PostConstruct
内でその構成されたオブジェクトに対しての初期化処理を記述することで関心を分離させられます。
2つめは、@PostConstruct
では全ての依存が注入されているのが保証されています。
前回のサンプルではコンストラクタへのインジェクションを行いましたが、Ray.Di ではSetterを使ったインジェクションも可能です。コンストラクタでも、Setterでも注入するという点では同じですし、挙動も基本的に変わりませんが、Setterインジェクションを行うことで、traitを利用しやすくなります。(実際に、BEAR.Sundayではtraitを利用して、テンプレートエンジンを注入したり、様々な箇所で使用されています。)
Ray.Diの内部の挙動は、コンストラクタへの注入後に、Setterへの注入が行われ、最後に @PostConstract
が呼ばれます。
たとえば、複数のSetterインジェクションがあるような場合、それらの複合的な設定を行うには、全ての注入が保証されている @PostConstruct
内でしか初期化できない処理も考えられるでしょう。
3つめは、これは副次的な特徴ですが @PostConstruct
では、同時にインターセプタを利用することが可能です。(逆に言えば、コンストラクタ内から別メソッド呼び出してもインターセプトされません。)
Ray.Di で、Injector::getInstance()
を行うと、そのクラスの依存が解決した後に、依存を注入しオブジェクトを構成させ、その後にウィーバー(インターセプトを織り込む)され、最後に @PostConstruct
が呼び出されます。
そのため、このアノテーション内では同時にインターセプトを利用することもできる、というわけです。
モジュールの作り方について
モジュールの分割単位は1)機能の関心ごと2)動作モード などで分割します。sandboxアプリではDevModule(動作モード)がフレームワークモジュール、機能のモジュール、アプリケーションモジュール等をinstallしてます。
— BEAR.Sundayさん (@BEARSunday) 7月 12, 2012
機能はたとえばDbなら Database\PdoModule にするのか Database\DoctrineOrm にするのかといった具合です。そのモジュール内では必要なDI、AOP設定を行います。
— BEAR.Sundayさん (@BEARSunday) 7月 12, 2012
とあるように、モジュールは関心ごとに分離すべきです。(今回はサンプルなので…)
Ray.Diには、あるモジュールの中から他のモジュールをインストール(追加設定のような感じ)する機能があります。
機能・関心ごとにモジュールを分離させることで、たとえば、実行(本番、テスト、開発)モードが要求する機能や関心を、必要に応じて構成させることができるようになります。
これは、下のつぶやきにもあるように、BEAR.Sundayの大きな特徴の一つです。
ここが他のFWのイベントによる機能拡張と違うところで、FW内のコードでイベント(シグナル)を発行してApp側でコールバックをアタッチするのではなくて、サービスコンポーネントそのものをDIで入れ替え、その振る舞いをAOPで変更します
— BEAR.Sundayさん (@BEARSunday) 7月 12, 2012
すべてのメソッドにインターセプタをバインドしてみる
では、ようやく本編です。
処理時間を計るタイマーインターセプタを作り、それをすべてのメソッドへ問答無用で追加(バインド)してみたいと思います。 メソッドへ適用するインターセプタは Ray\Aop\MethodInterceptor
を継承して作ります。
メソッドが呼び出されると、invoke()
が呼び出されるのでその中で前後に挟み込みたい処理を記述します。
今回は以下のように作りました。
src/Madapaja/Ray/Di/Sample01/Interceptor/Timer.php
namespace Madapaja\Ray\Di\Sample01\Interceptor;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
class Timer implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
printf('%s::%s(): Timer start'."\n", $invocation->getMethod()->class, $invocation->getMethod()->name);
$mtime = microtime(true);
$return = $invocation->proceed();
$time = microtime(true) - $mtime;
printf('%s::%s(): Timer stop [%01.7f sec]'."\n\n", $invocation->getMethod()->class, $invocation->getMethod()->name, $time);
return $return;
}
}
処理としては、メソッド開始前後にメソッド名とタイマー状況のアウトプットと、計測処理を行っているだけですね。
invoke
メソッドは呼び出される際に引数としてメソッド実行に必要な知識を持ったオブジェクト(MethodInvocation)が渡されます。
その知識を利用してメソッド名などを表示させ、また、14行目で $invocation->proceed()
することでオリジナルのメソッドを実行します。
さあ、インターセプタが出来たのでそれをバインドします。
前回説明したとおり、アプリケーションやフレームワークを構成する際に使われる知識はモジュールが持ちます。UserModule
にインターセプタをバインドする処理を追加します。
src/Madapaja/Ray/Di/Sample01/Module/UserModule.php
namespace Madapaja\Ray\Di\Sample01\Module;
use Ray\Di\AbstractModule;
use Madapaja\Ray\Di\Sample01\Interceptor\Timer;
class UserModule extends AbstractModule
{
protected function configure()
{
// bind dependency @Inject @Named("pdo_user")
$pdo = new \PDO('sqlite::memory:', null, null);
$this->bind('PDO')->annotatedWith('pdo_user')->toInstance($pdo);
// bind aspect Timer
$this->bindInterceptor(
$this->matcher->any(),
$this->matcher->any(),
[new Timer]
);
}
}
17行目から bindInterceptor
メソッドでインターセプタをバインドしています。 このメソッドは 3 つの引数を取り、1つ目はクラスのマッチャーを、2つめはメソッドのマッチャーを、3つめはインターセプターの配列を渡します。
1つめと、2つめの引数に $this->matcher->any()
を渡すことで、すべてのメソッドに適用されるようになります。
この状態で、php main.php
として実行してみると、タイマーインターセプタがバインドされていることが分かるかと思います。
Madapaja\Ray\Di\Sample01\Model\User::init(): Timer start
Madapaja\Ray\Di\Sample01\Model\User::init(): Timer stop [0.0005341 sec]
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer start
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer stop [0.0001001 sec]
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer start
:
:
アノテーションを使って特定のメソッドにインターセプタをバインドしてみる
次に、トランザクションを行うインターセプタを作って特定のメソッドへのバインドをしてみます。 特定のメソッドへのバインドは、メソッド名のプレフィックスやアノテーションでマッチさせることができますが、今回は汎用的に使えるようにアノテーションでマッチさせるようにします。
まずはタイマーの時と同じように、トランザクションインターセプタを作ります。
src/Madapaja/Ray/Di/Sample01/Interceptor/Transaction.php
namespace Madapaja\Ray\Di\Sample01\Interceptor;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
/**
* Transaction interceptor
*/
class Transaction implements MethodInterceptor
{
public function invoke(MethodInvocation $invocation)
{
$object = $invocation->getThis();
$ref = new \ReflectionProperty($object, 'db');
$ref->setAccessible(true);
$db = $ref->getValue($object);
$db->beginTransaction();
try {
echo "begin Transaction" . json_encode($invocation->getArguments()) . "\n";
$invocation->proceed();
$db->commit();
echo "commit\n";
} catch (\Exception $e) {
$db->roleback();
}
}
}
15行目で $invocation->getThis()
し、実行されるオブジェクト(User オブジェクト)を取得しています。 トランザクションを行うために User::$db
へのアクセスを行いたいのですが、このプロパティはプライベートプロパティのため、そのままアクセスすることができません。
そこで 16行目でリフレクションプロパティを取得し、プロパティをアクセス可能にした上でトランザクション処理を行っています。
次に、このインターセプタを適用させるアノテーションを作ります。
今回は @Transactional
アノテーションという名前で以下のように作りました。
src/Madapaja/Ray/Di/Sample01/Annotation/Transactional.php
namespace Madapaja\Ray\Di\Sample01\Annotation;
use Ray\Di\Di\Annotation;
/**
* @Annotation
* @Target("METHOD")
*/
final class Transactional implements Annotation
{
}
作ったアノテーションを User
クラスに適用させましょう。
src/Madapaja/Ray/Di/Sample01/Model/User.php
namespace Madapaja\Ray\Di\Sample01\Model;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\Di\Di\PostConstruct;
use Madapaja\Ray\Di\Sample01\Annotation\Transactional;
class User
{
// ...
/**
* @Transactional
*/
public function createUser($name, $age)
{
$sth = $this->db->prepare('INSERT INTO User (Name, Age) VALUES (:name, :age)');
return $sth->execute(array(':name' => $name, ':age' => $age));
}
// ...
use してアノテーションするだけです。
最後に UserModule
でバインドします。
src/Madapaja/Ray/Di/Sample01/Module/UserModule.php
namespace Madapaja\Ray\Di\Sample01\Module;
use Madapaja\Ray\Di\Sample01\Interceptor\Timer;
use Madapaja\Ray\Di\Sample01\Interceptor\Transaction;
use Ray\Di\AbstractModule;
class UserModule extends AbstractModule
{
protected function configure()
{
// ...
// bind aspect @Transactional method
$this->bindInterceptor(
$this->matcher->any(),
$this->matcher->annotatedWith('Madapaja\Ray\Di\Sample01\Annotation\Transactional'),
[new Transaction]
);
}
}
18行目で、メソッドに @Transactional
アノテーションが指定されている場合にマッチするように、annotatedWith()
マッチャーを使ってバインドします。
これでトランザクションインターセプタがバインドできましたので、php main.php
として実行してみましょう。
Madapaja\Ray\Di\Sample01\Model\User::init(): Timer start
Madapaja\Ray\Di\Sample01\Model\User::init(): Timer stop [0.0005541 sec]
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer start
begin Transaction["Koriym",30]
commit
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer stop [0.0001891 sec]
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer start
begin Transaction["Bear",22]
commit
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer stop [0.0001378 sec]
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer start
begin Transaction["Yoshi",25]
commit
Madapaja\Ray\Di\Sample01\Model\User::createUser(): Timer stop [0.0001421 sec]
Madapaja\Ray\Di\Sample01\Model\User::readUsers(): Timer start
Madapaja\Ray\Di\Sample01\Model\User::readUsers(): Timer stop [0.0000980 sec]
:
:
このように @Transactional
アノテーションをした User::createUser()
メソッドのみにトランザクションが行われているのが分かるかと思います。
とりあえず、サンプルを通じて Ray の触りをご紹介できたと思います。
インターセプター面白いです。
参考リンク
PHP5.4+フレームワーク BEAR.Sundayを理解するためにRay.Diを触ってみるの巻シリーズ