束縛(バインディング)

束縛またはバインディング(英: Binding)は、情報工学において、より大きく複雑で頻繁に使われる何かへの単純な参照の生成を意味する。単純な参照を大きなものを反復する代わりに使うことができる。束縛とはそのような参照を指す。また、そこから転じて、何らかの「関連付け」を束縛またはバインディングと称する。 –wikipedia 束縛 (情報工学)

BEAR.Sundayではコンパイルタイムとランタイムの明確な区別があります。ブートストラップで実行に必要なアプリケーションオブジェクトやページリソースオブジェクトを作るのがコンパイルです。

オブジェクトの依存がDIで解決され、リクエスト毎に変わらないオブジェクトの結びつきはここで決定されます。ブートストラップ時に束縛するので早期束縛、あるいは束縛が固定されているので静的束縛と呼びます1 が同じものです。(英語ではそれぞれEager Binding, Static Bindining

ランタイムではコンパイルで作られたオブジェクトのリクエスト毎に変わる処理が実行されます。コンパイルで決定できない依存はここでAOPを使って解決されます。利用メソッドが呼ばれる直前のギリギリのタイミングで遅延束縛、あるいは動的束縛(英語ではそれぞれLazy Binding, Dynamic Binding)と呼びます。

  1. Dependency Injectorがインジェクト =早期束縛
  2. AOPのアスペクトがインジェクト =遅延束縛

基本的に早期束縛を優先して、早期束縛できない時のみ遅延束縛を選ぶようにします。ブートストラップで依存を解決する早期束縛はオブジェクトとオブジェクトの結びつきを固定化させコード実行をより少なくします。速度もより高速です。

早期束縛(DI)できないサービスオブジェクト

以下のものはDIできません。AOPのアスペクトでインジェクトするか、またはプロバイダーというわれるマイクロファクトリーで都度生成します。

  1. シリアライズできないもの(PDO、クロージャ、リフレクションなどの組み込みオブジェクト等)
  2. コンストラクタが毎リクエスト生成を前提としてるもの(現在時刻をコンストラクタでプロパティに代入してるmonolog等)
  3. ランタイムでないと決定できないオブジェクト

DBオブジェクトのインジェクトはこのうち1番目と3番目にあたります。PHPはリソース変数がシリアライズできません、それに複数のリクエストメソッドを1つのクラスで表すリソースオブジェクトは、実際にメソッドが呼ばれないとどのDBオブジェクトをインジェクトするか(マスター/スレーブ)が決定できません。DBオブジェクトはアスペクト(横断段的処理)としてメソッドに束縛されたインターセプターでインジェクトします。

DBインジェクター

以下はDBインジェクターのコードです。これはリクエストメソッドの直前にコールされるインターセプターです。
AOPについての初歩的なことはマニュアルのはじめてのアスペクトをご覧下さい。

<?php
/**
 * This file is part of the BEAR.Sunday package
 *
 * @license http://opensource.org/licenses/bsd-license.php BSD
 */
namespace BEAR\Package\Interceptor;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Logging\SQLLogger;
use Ray\Aop\MethodInterceptor;
use Ray\Aop\MethodInvocation;
use Doctrine\Common\Annotations\AnnotationReader as Reader;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
/**
 * Cache interceptor
 *
 * @package    BEAR.Sunday
 * @subpackage Intercetor
 */
final class DbInjector implements MethodInterceptor
{
    /**
     * @var Reader
     */
    private $reader;
    /**
     * DSN for master
     *
     * @var array
     */
    private $masterDb;
    /**
     * DSN for slave
     *
     * @var array
     */
    private $slaveDb;
    /**
     * Set annotation reader
     *
     * @param Reader $reader
     *
     * @return void
     * @Inject
     */
    public function setReader(Reader $reader)
    {
        $this->reader = $reader;
    }
    /**
     * Constructor
     *
     * @param  array $masterDb
     * @@param array $slaveDb
     *
     * @Inject
     * @Named("masterDb=master_db,slaveDb=slave_db")
     */
    public function __construct(array $masterDb, array $slaveDb)
    {
        $this->masterDb = $masterDb;
        $this->slaveDb = $slaveDb;
    }
    /**
     * (non-PHPdoc)
     * @see Ray\Aop.MethodInterceptor::invoke()
     */
    public function invoke(MethodInvocation $invocation)
    {
        $object = $invocation->getThis();
        $method = $invocation->getMethod();
        $connectionParams = ($method->name !== 'onGet') ? $this->slaveDb : $this->masterDb;
        $pagerAnnotation = $this->reader->getMethodAnnotation($method, 'BEAR\Sunday\Annotation\DbPager');
        if ($pagerAnnotation) {
            $connectionParams['wrapperClass'] = 'BEAR\Package\Module\Database\DoctrineDbalModule\Connection';
            $db = DriverManager::getConnection($connectionParams);
            $db->setMaxPerPage($pagerAnnotation->limit);
        } else {
            $db = DriverManager::getConnection($connectionParams);
        }
        /* @var $db \BEAR\Package\Module\Database\DoctrineDbalModule\Connection */
        $object->setDb($db);
        $result = $invocation->proceed();
        if ($pagerAnnotation) {
            $pagerData = $db->getPager();
            if ($pagerData) {
                $object->headers['pager'] = $pagerData;
            }
        }
        return $result;
    }
}

このDBインジェクターはメソッドに応じてMaster/Slave DBを選択しています。つまりDBオブジェクトを利用するにはクラスに@Dbとマークするだけで、メソッドがコールされた直前のタイミングでマスタースレーブが自動選択されDBオブジェクトがインジェクトされます。

$connectionParams =
 ($method->name !== 'onGet') ? $this->slaveDb : $this->masterDb;

またもし`@DbPager`とメソッドがアノテートされてるとDBPager用のオブジェクトを作成しています。

        if ($pagerAnnotation) {
            $connectionParams['wrapperClass'] = 'BEAR\Package\Module\Database\DoctrineDbalModule\Connection';
            $db = DriverManager::getConnection($connectionParams);
            $db->setMaxPerPage($pagerAnnotation->limit);

DI/AOP開発プロセス

この記事ではこのDBインジェクターにロガーを追加します。既存のオブジェクトに機能を追加する時にどのようなプロセスが必要か明らかにします。

セッターメソッド

DBインジェクターインターセプターでアノテーションリーダーがインジェクトされているようにロガーを受け取るコードを記述します。

    /**
     * Set SqlLogger
     *
     * @param \Doctrine\DBAL\Logging\SQLLogger $sqlLogger
     *
     * @Inject(optional = true)
     */
    public function setSqlLogger(SQLLogger $sqlLogger)
    {
        $this->sqlLogger = $sqlLogger;
    }

このインジェクトを必須にしないでオプションにするために@Inject(optional = true)としています。SQLLoggerインターフェイスへの束縛が行われていれば(DI設定がされていれば)インジェクトされるし、なければされません。ログの要不要に応じて、例えば開発かプロダクションでその束縛を変更することができます。

インターフェイスと実装をモジュールで束縛

ここで開発時のみにロガーを利用するこtにします。DevModuleにこの記述を加えます。

$this->bind('Doctrine\DBAL\Logging\SQLLogger')->to('Doctrine\DBAL\Logging\EchoSQLLogger');

これでDoctrine\DBAL\Logging\SQLLoggerインターフェイスのオブジェクトを受け取るセッターにDoctrine\DBAL\Logging\EchoSQLLoggerオブジェクトがインジェクトされます。

特定メソッドとインターセプターの束縛

DBインジェクターは以下のようなコードでクラスに@Dbとアノテートしていて’on’で始まる全てのメソッドに束縛されてます。(このコードはBEAR\Package\Module\Database\DoctrineDbalModuleで見つける事ができます)

    private function installDbInjector()
    {
        $dbInjector = $this->requestInjection('\BEAR\Package\Interceptor\DbInjector');
        $this->bindInterceptor(
            $this->matcher->annotatedWith('BEAR\Sunday\Annotation\Db'),
            $this->matcher->startWith('on'),
            [$dbInjector]
        );
    }

アノテーション

アノテーションはDoctrine\Commonsのアノテーションライブラリを利用しています。
@Dbアノテーションはこのようなクラスです。このアノテーションは@Target(“CLASS”)とアノテートされ、クラスのみアノテートすることができます。メソッドには記述できません。2

<?php
/**
 * This file is part of the BEAR.Sunday package
 *
 * @package BEAR.Sunday
 * @license http://opensource.org/licenses/bsd-license.php BSD
 */
namespace BEAR\Sunday\Annotation;
/**
 * Db
 *
 * @Annotation
 * @Target("CLASS")
 *
 * @package    BEAR.Sunday
 * @subpackage Annotation
 */
final class Db implements Annotation
{
}

インターセプターで利用

これでインターセプターのセッターメソッドはロガーを受け取りました。この依存を受け取る方法は依存逆転原則に従ってインターフェイスに依存しています。モジュールでの束縛を変更しても利用オブジェクトには変更がありません。

利用コードではもしロガーがあればセットするというコードを記述しました。

        if ($this->sqlLogger instanceof SQLLogger) {
            $db->getConfiguration()->setSQLLogger($this->sqlLogger);
        }

結論

これでDBオブジェクトにロガーがセットされて、SQLを実行するたびにSQLが表示されるようになりました。しかしEchoSQLLoggerはその名の通り、SQLを即時にechoするもので洗練されたログ機構とはとてもいえません。DebugStackというログをスタックとして記録するクラスを代わりに束縛し、ZF\Logや他のログライブラリがその情報を利用するようなコードが必要でしょう。

この記事では2つの束縛の違いと横断的処理であるインターセプターも依存を受け取る事が出来る事、それをオプションにしたりモードに応じてインジェクト内容を変えれる事、インターセプターとメソッドの束縛、受け取った依存の利用という一連の流れを説明しました。

依存逆転原則に従って抽象に依存したオブジェクトを静的にDIで繋げ、AOPで横断的処理の束縛を動的にできる例をみました。関心は分離されその結合は疎です。BEAR.Sundayのフレームワーク機能はほぼ全てこの原則と手順で構成され、BEAR.Sundayアプリケーションも同様です。

共通基底クラスや共通規約、固定的なメソッドやプロパティを使う事なくオブジェクトの構成を同一原則で行っています。そのBEAR.Sundayのオブジェクトを構成する原則もBEAR.Sunday固有のものでなく、ソフトウエア技術として一般性を持ち支持を受けている原理・法則で構成されたものです。

  1. BEAR.Sundayではこの束縛がリクエストを超えて再利用されます []
  2. これらの規則はDoctrineアノテーションによるものです []