はじめに
BEAR.Sundayを使った開発では、普段PHPを使って行う開発では意識することがあまりない、「コンパイルタイム*1」と「ランタイム」という概念*2を意識して開発することが必要となる場合があります。
DIとAOP
DIもAOPもオブジェクトの接続に着目した技術です。
DIはオブジェクトの構成、AOPはオブジェクトの相互作用、それぞれ2つの視点からオブジェクトを接続します。
今回はDIとAOP、それぞれの動作を**「コンパイルタイム」と「ランタイム」**という2つの視点*2に分けて考えてみます。
DIで注入して良い値と悪い値
これはTwitterの認証を行うために、ZendOAuth\Consumerを利用するDIの例です。
<?php
namespace My\App\Resource\Page\Auth;
use BEAR\Resource\ResourceObject;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use ZendOAuth\Consumer;
/**
* Class Twitter
* @package My\App\Resource\Page\Auth
*/
class Twitter extends ResourceObject
{
/**
* @var Consumer
*/
private $oauthConsumer;
/**
* @param Consumer $consumer
* @Inject
* @Named("twitter")
*/
public function __construct(Consumer $consumer)
{
$this->oauthConsumer = $consumer;
}
}
適切にModuleが設定されていれば問題なくZendOAuth\Consumerがインジェクトされると思います。
実際にTwitterにOAuthリクエストを行うためには、Sessionにリクエストトークンを保存する必要があります。
このとき、SessionをZendOAuth\Consumerと同様に以下のように扱いたくなると思いますが、このように扱ってはいけません。
<?php
namespace My\App\Resource\Page\Auth;
use BEAR\Resource\ResourceObject;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use ZendOAuth\Consumer;
use Aura\Session\SegmentInterface AS Session;
/**
* Class Twitter
* @package My\App\Resource\Page\Auth
*/
class Twitter extends ResourceObject
{
/**
* @var Consumer
*/
private $oauthConsumer;
/**
* @param Consumer $consumer
* @param Session $session
* @Inject
* @Named("consumer=twitter")
*/
public function __construct(Consumer $consumer, Session $session)
{
$this->oauthConsumer = $consumer;
$this->session = $session;
}
}
Aura.SessionではSessionManagerインスタンスを作成する際に、$_COOKIEなどの依存性を引数に渡して作成します。
したがって、そのインスタンスをインジェクトするということは、セッションIDなど内部の状態を保ったままシリアライズ(保存)されてしまうということを意味*3します。
AOPを使ったランタイムインジェクション
このような実行時に動作を決定したいインスタンスはどのように扱えば良いでしょうか?
DIをコンパイルタイムの一度しか実行されない静的な構成、AOPをランタイムの処理が実行される毎に呼び出される動的な処理と捉え、この問題をAOPで解決してみます。
Annotation
アノテーションを作成します。
このアノテーションでマークされているPageリソースのメソッドが呼ばれる際に、PageリソースのフィールドにSessionが自動で注入されるようなシナリオを想定します。
<?php
namespace My\App\Annotation;
use BEAR\Resource\Annotation\AnnotationInterface;
/**
* @Annotation
* @Target("METHOD")
*/
final class Session implements AnnotationInterface
{
}
Interceptor
実際にSessionを注入するインターセプターを作ります。
今回はAura.Sessionを利用しています。
<?php
namespace My\App\Interceptor\Session;
use Aura\Session\Segment;
use Ray\Aop\MethodInterceptor;
use Aura\Session\Manager;
use Aura\Session\SegmentFactory;
use Aura\Session\CsrfTokenFactory;
use Aura\Session\Randval;
use Aura\Session\Phpfunc;
use Ray\Aop\MethodInvocation;
/**
* Class SessionInjector
* @package My\App\Module\App\Session
*/
class SessionInjector implements MethodInterceptor
{
/**
* @var Segment
*/
private $session;
/**
* @var Manager
*/
private $manager;
/**
* @param MethodInvocation $invocation
* @return mixed
*/
public function invoke(MethodInvocation $invocation)
{
$object = $invocation->getThis();
if (!isset($this->manager)) {
$this->manager = new Manager(
new SegmentFactory,
new CsrfTokenFactory(
new Randval(
new Phpfunc
)
),
$_COOKIE
);
$this->session = $this->manager->newSegment('My\App');
}
$object->setSession($this->session, $this->manager);
return $invocation->proceed();
}
}
trait
上記インターセプターではオブジェクトに対して、setSessionというメソッドを呼び出すことでSessionを注入しています。
このままではSessionを利用する時に、毎回PageリソースにsetSessionメソッドを実装しないといけなくなってしまいますので、traitにして合成できるようにします。
<?php
namespace My\App\Module\App\Session;
use Aura\Session\Manager;
use Aura\Session\SegmentInterface;
/**
* Session client setter
*/
trait SessionInject
{
/**
* @var SegmentInterface
*/
private $session;
/**
* @var Manager
*/
private $sessionManager;
/**
* @param SegmentInterface $session
* @param Manager $manager
*/
public function setSession(SegmentInterface $session, Manager $manager)
{
$this->session = $session;
$this->sessionManager = $manager;
}
}
Module
これで準備は整いました。Moduleでバインドを行います。
作成したらメインモジュール(Module/AppModule.php)等でinstallして下さい。
<?php
namespace My\App\Module\App;
use Ray\Di\AbstractModule;
use Ray\Di\Scope;
/**
* Class SessionModule
* @package My\App\Module\App
*/
class SessionModule extends AbstractModule
{
public function configure()
{
$this->bindInterceptor(
$this->matcher->any(),
$this->matcher->annotatedWith('My\App\Annotation\Session'),
[$this->requestInjection('My\App\Interceptor\Session\SessionInjector')]
);
}
}
Consumer
利用時です。
traitのuse宣言とメソッドの@Sessionアノテーションに注目して下さい。
この2つを付与することで、メソッドが呼び出される度にSessionがフィールドに自動でインジェクトされるようになります。
<?php
namespace My\App\Resource\Page\Auth;
use BEAR\Resource\ResourceObject;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use My\App\Annotation\Session;
use My\App\Module\App\Session\SessionInject;
use ZendOAuth\Consumer;
/**
* Class Twitter
* @package My\App\Resource\Page\Auth
*/
class Twitter extends ResourceObject
{
use SessionInject;
/**
* @var Consumer
*/
private $oauthConsumer;
/**
* @param Consumer $consumer
* @Inject
* @Named("twitter")
*/
public function __construct(Consumer $consumer)
{
$this->oauthConsumer = $consumer;
}
/**
* @return $this
* @Session
*/
public function onGet()
{
$token = $this->oauthConsumer->getRequestToken();
/** @noinspection PhpUndefinedFieldInspection */
$this->session->twitterRequestToken = serialize($token);
$this->oauthConsumer->redirect();
}
}
最後に
DIとAOPを**「コンパイルタイムとランタイム」という視点*2から敢えて単純に捉える**ことで、それぞれの特性を活かした利用シーンを考える手助けになると思います。
※今回のサンプルはBEAR.PackageのDBAL関連モジュールを手本に作成しています。
1 一般的な用語ではありませんが、ランタイムと対照して便宜的にそう呼びます。一般的に利用されている用語のようです。訂正します。ご指摘ありがとうございます。
2 DIとAOPという概念がそれぞれ、必ずしもコンパイルタイムとランタイムに対してのみ作用するということではありません。
3 現在のBEAR.Sundayのバージョンでは(2014/12/9現在)CacheInjectorを利用しない限りこのような動作にはなりません。