初めてのDIフレームワーク

準備

  1. PHP5.4+で動作します。mysqlで予めテーブルを作成しておきます。 2 フォルダをつくります。
$ mkdir ray.tutorial
$ cd ray.tutorial

まずは手動でインジェクションするコードソースを入力して実行します。

<?php
class Todo
{
    /**
     * @var PDO
     */
    private $pdo;

    /**
     * @param PDO $pdo
     */
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * @param string $todo things to do
     */
    public function add($todo)
    {
        $stmt = $this->pdo->prepare('INSERT INTO TODO (todo) VALUES (:todo)');
        $stmt->bindParam(':todo', $todo);
        $stmt->execute();
    }
}
$pdo = new PDO('mysql:dbname=test;host=localhost');
$todo = new Todo($pdo);
$todo->add('Get laundry');

実行してみます。

$ php manual-di.php

データベースにtodoが入力されたか、コンソールかツール等で確認します。1 確認できましたか?OK? では、次にcomposerのプロジェクトを作ってこのクラスをDI化してみましょう。

composerでRay.Di依存の空プロジェクトを作る

まずはcomposerをダウンロードします。

$ curl -sS https://getcomposer.org/installer | php

composerを使ってRay.Diを使うプロジェクトを作ります。

$ php composer.phar init

すると色々質問されるので、ray/diのバージョン* (最新の安定板)をインストールするように答えます。

  Welcome to the Composer config generator
This command will guide you through creating your composer.json config.
Package name (<vendor>/<name>) [akihito/ray.tutorial]:
Description []:
Author [Akihito Koriyama <akihito .koriyama@gmail.com>]:
Minimum Stability []:
License []:
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]?
Search for a package []: ray/di
Found 15 packages matching ray/di
   [0] ray/di
   [1] ray/aop
   [2] jms/di-extra-bundle
   [3] aura/di
   [4] orno/di
   [5] league/di
   [6] mnapoli/php-di
   [7] zendframework/zend-di
   [8] mnapoli/php-di-zf1
   [9] ocramius/ocra-di-compiler
  [10] lcobucci/di-builder
  [11] aimfeld/di-wrapper
  [12] kdyby/autowired
  [13] seiffert/console-extra-bundle
  [14] vojtech-dobes/extensions-list
Enter package # to add, or the complete package name if it is not listed []: 0
Enter the version constraint to require []: *
Search for a package []:
Would you like to define your dev dependencies (require-dev) interactively [yes]?
Search for a package []:
{
    "name": "akihito/ray.tutorial",
    "require": {
        "ray/di": "*"
    },
    "authors": [
        {
            "name": "Akihito Koriyama",
            "email": "akihito.koriyama@gmail.com"
        }
    ]
}
Do you confirm generation [yes]?
</akihito></name></vendor>

入力の必要な質問はこれだけでした。

Search for a package []: ray/di
Enter package # to add, or the complete package name if it is not listed []: 0
Enter the version constraint to require []: *

すると最後に表示されたcomposer.jsonが出来上がりますが、まだray/diはインストールされていません。installコマンドでインストールします。

$ php composer.phar install

initコマンドで作成したcomposer.jsonに従ってRay.Diとその依存ファイルとダウンロードされ、現在の依存の状態が記録されたcomposer.lockファイル、それにautoloaderを含むcomposerのファイル群もインストールされました。

$ tree -L 2
├── composer.json
├── composer.lock
├── composer.phar
└── vendor
    ├── aura
    ├── autoload.php
    ├── composer
    ├── doctrine
    └── ray

Ray.Diを使ったコードを入力してsrc/フォルダを作ってその下に配置します。 src/todo3-ray-di.php

<?php
use Doctrine\Common\Annotations\AnnotationRegistry;
use Ray\Di\AbstractModule;
use Ray\Di\Injector;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
$loader = require dirname(__DIR__) . '/vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
class Todo
{
    private $pdo;
    /**
     * @Inject
     */

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * @param $todo
     */
    public function add($todo)
    {
        $stmt = $this->pdo->prepare('INSERT INTO TODO (todo) VALUES (:todo)');
        $stmt->bindParam(':todo', $todo);
        $stmt->execute();
    }
}
class Module extends AbstractModule
{
    public function configure()
    {
        $pdo = new PDO('mysql:dbname=test;host=localhost');
        $this->bind('PDO')->toInstance($pdo);
    }
}
$injector = Injector::create([new Module]);
$todo = $injector->getInstance('Todo');
/** @var $todo Todo */
$todo->add('Walking in Ray');

これがRay.Diを使ってDIを行っているコードです。変わった部分をそれぞれ見て行きます。

オートローダー

$loader = require dirname(__DIR__) . '/vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);

composerを使うと依存ファイルのオートローディングの設定が含まれた、vendor/autoload.phpというオートローダーのファイルが自動で生成されます。 Ray.DiのアノテーションはDoctrineのアノテーションを使っています。アノテーションの読み込みにはオートローダーの登録が必要で、いくつかの方法がありますがここではcomposerのオートローダーをそのまま使っています。

アノテーション

    /**
     * @Inject
     */
    public function __construct(PDO $pdo)
    {

依存を受け取るメソッドには@Injectとアノテート(注釈)されています。Ray.Diはこのアノテーションを目印にして依存が必要なメソッドを割り出します。2 アノテーションはクラスで、名前解決のためuse文が必要です。3

use Ray\Di\Di\Inject;

モジュール

モジュールでは依存を必要とする場所に依存をどう渡すかを記述します。

class Module extends AbstractModule
{
    public function configure()
    {
        $pdo = new PDO('mysql:dbname=test;host=localhost');
        $this->bind('PDO')->toInstance($pdo);
    }
}

AbstractModuleを継承したクラスのconfigure()というメソッド内で、bind()メソッドを使って依存を束縛(バインド=結びつけます)します。ここではPDOクラスを必要とするインジェクションポイントに作成した$pdoインスタンスを束縛しています。 これによってアノテーションの節で説明したように@injectとアノテートされPDOクラスのタイプヒントを持つ引き数には$pdoインスタンスが渡されるようになります。

インジェクター

モジュールを使って作成したインジェクターは、どの依存が求められれば何を渡せばいいかを知っています。そのインジェクターを使って’Todo’クラスを取得するとインジェクターは必要とされる依存をモジュールで決めたルールで渡し、依存解決(dependency resolution)が行われます。

$injector = Injector::create([new Module]);
$todo = $injector->getInstance('Todo');

ついに出来ました!!!! $todoオブジェクト!!! 依存の問題を解決(外部の変数を外側から渡す)を自動化するために、様々な事が必要になりました。 依存が必要な箇所にアノテーションが必要です。そのアノテーションクラスのオートローディング登録も必要で、モジュールでも依存の束縛の記述、束縛を使ったインジェクターの作成をしてようやく依存解決をするインジェクターが作成されました。 1つの問題を解決するためにこれだけの事をしたのです。4DIフレームワークはRayだけではありません。他のDIフレームワークも同じような、あるいはこれ以上の準備の手順の複雑さを持っています。

オーバーエンジニアリング?

オーバーエンジニアリング(作り込みのし過ぎ、過剰技術)でしょうか? まず、他の技術同様に、説明のための単純な例で実利を感じる事は往々にして難しい事は頭に入れておく必要があります。例えば、HelloWorldのサンプルでフレームワークのメリットを実感する事はなかなか難しいでしょう。 DIフレームワークの使用がオーバーエンジアリングか、クラス名のハードコーディングがアンダーエンジニアリングなのか、その辺りの判断を直感で出すのはひとまず置いといて、Ray DIフレームワークの使い方の実例をもう少し見て行きましょう。 …続く

  1. あるいはSELECTをするメソッドを追加してください! []
  2. コンストラクタ以外でも依存を受け取る事ができます。@Injectとアノテートしてメソッド名は何でもかまいません []
  3. Doctrineアノテーションの仕様です []
  4. NateさんのLithiumのスライド A Framework for People who Hate Frameworks – Lithium もご覧下さい []