> 概要

Sabelコンテナでは、依存性の注入とアスペクトの適応が可能です。

> インジェクションタイプ

インジェクションタイプとは、[どのタイミングで依存性を注入するか]という種類です。
Sabelコンテナでは、下記のインジェクションタイプが使用できます。

コンストラクタインジェクションではインスタンスを生成するとき、コンストラクタへ依存を注入します。

・クラス定義

 
<?php
 
class Person
{
  public $name = "";
  private $partner = null;
  
  public function __construct($name, Partner $partner)
  {
    $this->name = $name;
    
    $this->partner = $partner;
    $this->partner->tag($this);
  }
}
 
<?php
 
interface Partner
{
  public function whoYourPartner();
  public function tag(Partner $partner);
}
 
<?php
 
class GoodPartner implements Partner
{
  private $partner = null;
  
  public function whoYourPartner()
  {
    echo $this->partner->name;
  }
  
  public function tag(Partner $partner)
  {
    $this->partner = $partner;
  }
}
 
<?php
 
class WrongPartner implements Partner
{
  public function whoYourPartner()
  {
    echo "another one.";
  }
  
  public function tag(Partner $partner)
  {
    unset($partner);
  }
}

上で定義したクラスに依存性注入を行なう設定クラスを定義します。

 
<?php
 
class PartnerSelection extends Sabel_Container_Injection
{
  public function configure()
  {
    $this->construct("Person")->with("hoge")->with("Partner");
    $this->bind("Partner")->to("GoodPartner");
  }
}
 
$person = Sabel_Container::load("Person", new PartnerSelection());
$person->bringYourPartner()->whoYourPartner(); // hoge
 
// $this->bind("Partner")->to("WrongPartner") に設定を変更
 
$person->bringYourPartner()->whoYourPartner(); // another one

Sabel_Container::load()はload()としても用意されていて、 $person = load("Person", new PartnerSelection());
として利用することもできます。

> アスペクト

SabelAspectでは、PHPでのアスペクト指向プログラミング(AOP)をサポートします。
現状のPHPでは言語としてアスペクトをサポートしていません。
SabelAspectはPHPの機能だけを用いてAOPを実現します。
インターセプト可能なジョインポイントはターゲットオブジェクトのメソッド実行時です。

コンテナと併用した場合の簡単な例

 
 
// コンテナ設定
class Config extends Sabel_Container_Injection
{
  public function configure()
  {
    $this->aspect("Target")->advice("Advice");
  }
}
 
// target
class Target
{
  public function getState()
  {
     return "state";
  }
}
 
// Advice
class Advice
{
  /**
   * @before get.+
   */
  public function beforeGet($method, $arguments, $target)
  {
    // メソッドが実行される直前に、このメソッドが実行される。
  }
  
  /**
   * @after get.+
   */
  public function afterGet($method, $arguments, $target, $result)
  {
    // メソッドが実行された後に、このメソッドが実行される。
  }
  
  /**
   * @around get.+
   */
  public function aroundGet($invocation)
  {
    // メソッドが実行される前に、このメソッドが実行される。
    
    // $invocationのproceed()メソッドを実行しなければ、ターゲットの対象メソッドが実行されることは無い
    $result = $invocation->proceed();
    
    return $result;
  }
  
  /**
   * @throws get.+
   */
  public function throwsGet($method, $arguments, $target, $result, Exception $exception)
  {
    // メソッドが実行され、例外が発生したらこのメソッドが実行される。
  }
}
 
// main
 
load("Target", new Config());

$this->aspect()で指定するターゲットには、実装クラスではなく、インタフェイスも指定できる、 インタフェイスを指定した場合には、ターゲットクラスがそのインタフェイスを実装していればアスペクトが織り込まれる

Weaverを直接使用する

Sabel Containerを使用しないで、アスペクトを直接使用します。

 
$weaver = new Sabel_Aspect_StaticWeaver();
$weaver->setTarget("TargetClass");
 
$advisor = new Sabel_Aspect_RegexMatcherPointcutAdvisor();
$advisor->setClassMatchPattern("/.+/");
$advisor->setMethodMatchPattern("/get+/");
 
$advisor->addAdvice(new Sabel_Aspect_DebugInterceptor());
 
$weaver->addAdvisor($advisor);
 
$target = $weaver->getProxy();

アドバイザーに登録するアドバイスは、Sabel_Aspect_MethodBeforeAdvice, Sabel_Aspect_MethodAfterAdvice, Sabel_Aspect_MethodThrowsAdviceを実装する必要があります。
Aroundとしてアドバイスを実装するには、Sabel_Aspect_MethodInterceptorを実装します。

アラウンドアドバイスの例

 
class PerformanceMonitorInterceptor implements Sabel_Aspect_MethodInterceptor
{
  public function invoke(Sabel_Aspect_MethodInvocation $invocation)
  {
    $start = microtime();
    
    $result = $invocation->proceed();
    
    l(microtime() - $s);
    
    return $result;
  }
}

Sabel_Aspect_MethodInvocationのproceed()メソッドを呼ぶと、後続のアドバイスが実行されます。
アドバイスが全て実行された後に、ターゲットオブジェクトの対象メソッドが実行されます。
また、このメソッドを呼ばずに、値を返却すると、その値が戻り値として扱われます。この時、後続のアドバイスやターゲットオブジェクトの対象メソッドは呼ばれません。

例えば、キャッシュ管理をアスペクトで行なう場合、対象オブジェクトの戻り値を使用せずに、キャッシュ値を返却したい場合などは、proceed()を呼ぶ必要はありません。

ビフォアアドバイスの例

 
class BeforeInterceptor implements Sabel_Aspect_MethodBeforeAdvice
{
  public function before($method, $arguments, $target)
  {
    // before
  }
}

このアドバイスはターゲットオブジェクトの対象メソッドが呼びだされる前に実行されます。
このメソッドで、null以外の値を返却すると、後続のアドバイスは実行されず、返却された値がクライアントオブジェクトに戻されます。

アフターアドバイスの例

 
class AfterInterceptor implements Sabel_Aspect_MethodAfterAdvice
{
  public function after($method, $arguments, $target, $result, $exception)
  {
    // after
  }
}

このアドバイスは、ターゲットオブジェクトの対象メソッドが例外を送出してもしなくても実行されます。
例外が送出された場合には、$exceptionがnullでない値になります。
ビフォアと同様にnull以外の値が返却された場合には、その値がクライアントオブジェクトに戻されます。

アフターリターニングアドバイスの例

 
class AfterReturningInterceptor implements Sabel_Aspect_MethodAfterReturningAdvice
{
  public function after($method, $arguments, $target, $result)
  {
    // after returning
  }
}

このアドバイスは、ターゲットオブジェクトのメソッド実行が正常に完了した場合のみ呼ばれます。 戻り値のルールについては、アフターやビフォア同様。

例外アドバイスの例

 
class ThrowsInterceptor implements Sabel_Aspect_MethodThrowsAdvice
{
  public function throws($method, $arguments, $target, $exception)
  {
    // 例外発生時
  }
}

例外発生時のみ呼ばれるアドバイスです。 null以外の値が返却された場合は、その値が戻されます。nullの場合には発生した例外がそのまま再送出されます。

メソッド呼出をトレースするサンプル

 
class Sabel_Aspect_SimpleTraceInterceptor extends Sabel_Aspect_AbstractTraceInterceptor
{
  public function invoke(Sabel_Aspect_MethodInvocation $invocation)
  {
    $logger = new Logger();
    $invocationDescription = $this->getInvocationDescription($invocation);
    $logger->trace("Entering: " . $invocationDescription);
    
    try {
      $rval = $invocation->proceed();
      $logger->trace("Exiting: " . $invocationDescription . " with " . var_export($rval, 1));
      return $rval;
    }catch (Exception $ex) {
      $logger->trace("Exception thrown in " . $invocationDescription, $ex);
      throw $ex;
    }
  }
  
  protected function getInvocationDescription($invocation)
  {
    $fmt = "method '%s' of class[%s]";
    return sprintf($fmt, $invocation->getMethod()->getName(),
                         $invocation->getThis()->getName());
  }
}

トランザクションをアスペクトで処理する

例えば下記の様なモデルクラスがあります。movePoint()メソッドには、トランザクションの開始や、 例外時のロールバック処理が記述されていますが、これは本来のドメインロジックとは関係がない処理です。 これをアスペクトで分離してみます。

 
class User extends Sabel_DB_Model
{
  public function movePoint()
  {
    Sabel_DB_Transaction::activate();  # トランザクション有効化
    
    try {
      $fromUser = MODEL("User", 1);
      if ($fromUser->point < $point) {
        throw new ...
      } else {
        $toUser = MODEL("User", 2);
        if ($toUser->isValid()) {
          ...
          
          Sabel_DB_Transaction::commit();  # コミット
        } else {
          throw new ...
        }
      }
    } catch (Exception $e) {
      Sabel_DB_Transaction::rollback();  # 例外が発生したらロールバック
      throw $e;
    }
  }
}

アドバイスクラス

 
class TransactionAdvice
{
  /**
   * @around movePoint
   */
  public function processTransaction($invocation)
  {
    Sabel_DB_Transaction::activate();  # トランザクション有効化
    
    try {
      $result = $invocation->proceed();
      
      Sabel_DB_Transaction::commit(); # 正常終了
      
      return $result;
    } catch (Exception $e) {
      Sabel_DB_Transaction::rollback();  # 例外が発生したらロールバック
      throw $e;
    }
  }
}

アドバイスにトランザクション処理を移したモデルクラス

 
class User extends Sabel_DB_Model
{
  public function movePoint()
  {
    $fromUser = MODEL("User", 1);
    
    if ($fromUser->point < $point) {
      throw new ...
    } else {
      $toUser = MODEL("User", 2);
      if ($toUser->isValid()) {
        ...
      } else {
        throw new ...
      }
    }
  }
}

ご覧の通り、モデルのmovePoint()メソッドは自分の責務のみに集中しています。