Ray.Di チュートリアル1

このチュートリアルでは、DIパターンの基礎やRay.Diのプロジェクトの始め方を学びます。 DIを使わないコードから手動のDIコードに変更し、次にRay.Diを使ったコードにして機能追加をします。

準備

チュートリアルのためのプロジェクトを作成します。

mkdir ray-tutorial
cd ray-tutorial
composer self-update
composer init --name=ray/tutorial --require=ray/di:^2 --autoload=src -n
composer update

src/Greeter.phpを作成します。 $usersに次々に挨拶するプログラムです。

<?php
namespace Ray\Tutorial;

class Greeter
{
    public function sayHello(): void
    {
        $users = ['DI', 'AOP', 'REST'];
        foreach ($users as $user) {
            echo 'Hello ' . $user . '!' . PHP_EOL;
        }
    }
}

実行するためのスクリプトをbin/run.phpに用意します。

<?php
use Ray\Tutorial\Greeter;

require dirname(__DIR__) . '/vendor/autoload.php';

(new Greeter)->sayHello();

実行してみましょう。

php bin/run.php

Hello DI!
Hello AOP!
Hello REST!

ディペンデンシー・プル

$usersを可変にする事を考えましょう。

例えば、グローバル変数?

-       $users = ['DI', 'AOP', 'REST'];
+       $users = $GLOBALS['users'];

ワイルドですね。 他の方法も考えてみましょう。

define("USERS", ['DI', 'AOP', 'REST']);

$users = USERS;
class User
{
    public const NAMES = ['DI', 'AOP', 'REST'];
};

$users = User::NAMES;
$users = Config::get('users')

必要な依存を外部から取得(dependency pull)していて、結局は$GLOBALS変数と同じグローバルです。オブジェクト間の結合を密にし、テストを困難にします。

ディペンデンシー・インジェクション

コードの外側から依存を注入(dependency injection)するのがDIパターンです。

class Greeter
{
    public function __construct(
        private readonly Users $users
    ) {}

    public function sayHello(): void
    {
        foreach ($this->users as $user) {
            echo 'Hello ' . $user . '!' . PHP_EOL;
        }
    }
}

必要なデータだけでなく、出力も独立したサービスにして注入しましょう。

    public function __construct(
-       private readonly Users $users
+       private readonly Users $users,
+       private readonly PrinterInterface $printer
    ) {}

    public function sayHello()
    {
        foreach ($this->users as $user) {
-            echo 'Hello ' . $user . '!' . PHP_EOL;
+            ($this->printer)($user);
        }
    }

以下のクラスを用意します。

src/Users.php

<?php
namespace Ray\Tutorial;

use ArrayObject;

final class Users extends ArrayObject
{
}

src/PrinterInterface.php

<?php
namespace Ray\Tutorial;

interface PrinterInterface
{
    public function __invoke(string $user): void;
}

src/Printer.php

<?php
namespace Ray\Tutorial;

class Printer implements PrinterInterface
{
    public function __invoke(string $user): void
    {
        echo 'Hello ' . $user . '!' . PHP_EOL;
    }
}

src/GreeterInterface.php

<?php
namespace Ray\Tutorial;

interface GreeterInterface
{
    public function sayHello(): void;
}

src/CleanGreeter.php

<?php
namespace Ray\Tutorial;

class CleanGreeter implements GreeterInterface
{
    public function __construct(
        private readonly Users $users,
        private readonly PrinterInterface $printer
    ) {}

    public function sayHello(): void
    {
        foreach ($this->users as $user) {
            ($this->printer)($user);
        }
    }
}

手動DI

これを実行するスクリプトbin/run_di.phpを作成して実行しましょう。

<?php

use Ray\Tutorial\CleanGreeter;
use Ray\Tutorial\Printer;
use Ray\Tutorial\Users;

require dirname(__DIR__) . '/vendor/autoload.php';

$greeter = new CleanGreeter(
    new Users(['DI', 'AOP', 'REST']),
    new Printer
);

$greeter->sayHello();

ファイル数が増え全体としては複雑になっているように見えますが、個々のスクリプトはこれ以上単純にするのが難しいぐらい単純です。それぞれのクラスはただ1つの責務しか担っていませんし1、実装ではなく抽象に依存して2、テストや拡張、それに再利用も容易です。

コンパイルタイムとランタイム

bin/以下のコードがコンパイルタイムで依存関係を構成し、src/以下のコードはランタイムで実行されます。PHPはスクリプト言語ですが、このようにコンパイルタイムとランタイムの区別を考えることができます。

コンストラクターインジェクション

DIのコードは依存を外部から渡して、コンストラクターで受け取ります。

$instance = new A(
    new B,
    new C(
        new D(
            new E, new F, new G
        )
    )
);

Aを生成するために必要なBとCは(A内部から取得しないで)Aの外部からコンストラクターに渡されています。Cを生成するにはDが、Dを生成するにはE,F,Gが..と依存は他の依存を必要とし、オブジェクトが依存オブジェクトを含むオブジェクトグラフ3が生成されます。

プロジェクトの規模が大きくなると、このようなファクトリーコードを使った手動のDIは、深いネストの依存解決、シングルトンなどのインスタンス管理、再利用性、メンテナンス性などの問題が現実化してきます。その依存の問題を解決するのがRay.Diです。

モジュール

モジュールは束縛の集合です。束縛にはいくつか種類がありますが、ここでは最も基本のインターフェースにクラスを束縛するリンク束縛 、バリューオブジェクトなど実態への束縛を行うインスタンス束縛を行います。

src/AppModule.phpを用意します。

<?php
namespace Ray\Tutorial;

use Ray\Di\AbstractModule;

class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(Users::class)->toInstance(new Users(['DI', 'AOP', 'REST']));
        $this->bind(PrinterInterface::class)->to(Printer::class);
        $this->bind(GreeterInterface::class)->to(CleanGreeter::class);
    }
}

実行するbin/run_di.phpを作成して、実行します。

<?php

use Ray\Di\Injector;
use Ray\Tutorial\AppModule;
use Ray\Tutorial\GreeterInterface;

require dirname(__DIR__) . '/vendor/autoload.php';

$module = new AppModule();
$injector = new Injector($module);
$greeter = $injector->getInstance(GreeterInterface::class);
$greeter->sayHello();

うまくいきましたか? おかしい時はtutorial1と見比べてみてください。

依存の置き換え

ユニットテストの時だけ、開発時だけ、など実行コンテキストによって束縛を変更したい時があります。

例えばテストの時だけの束縛src/TestModule.phpがあったとします。

<?php

namespace Ray\Tutorial;

use Ray\Di\AbstractModule;

final class TestModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(Users::class)->toInstance(new Users(['TEST1', 'TEST2']));
    }
}

この束縛を上書きするためにbin/run_di.phpスクリプトを変更します。

use Ray\Tutorial\AppModule;
+use Ray\Tutorial\TestModule;
use Ray\Tutorial\GreeterInterface;

require dirname(__DIR__) . '/vendor/autoload.php';

$module = new AppModule();
+$module->override(new TestModule());

実行してみましょう。

Hello TEST1!
Hello TEST2!

依存の依存

次に今Printerで固定している挨拶のメッセージも多国語対応するために注入するように変更します。

src/IntlPrinter.phpを作成します。

<?php
namespace Ray\Tutorial;

use Ray\Di\Di\Named;

class IntlPrinter implements PrinterInterface
{
    public function __construct(
        #[Message] private string $message
    ){}

    public function __invoke(string $user): void
    {
        printf($this->message, $user);
    }
}

コンストラクターは挨拶のメッセージ文字列を受け取りますが、この束縛を特定するためにアトリビュート束縛のための#[Message]アトリビュート、src/Message.phpを作成します。

<?php
namespace Ray\Tutorial;

use Attribute;
use Ray\Di\Di\Qualifier;

#[Attribute, Qualifier]
class Message
{
}

束縛を変更。

class AppModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(Users::class)->toInstance(new Users(['DI', 'AOP', 'REST']));
-       $this->bind(PrinterInterface::class)->to(Printer::class);
+       $this->bind(PrinterInterface::class)->to(IntlPrinter::class);
+       $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);
        $this->bind(GreeterInterface::class)->to(CleanGreeter::class);
    }
}

実行して変わらない事を確認しましょう。

次にエラーを試してみましょう。configure()メソッドの中のMessage::classの束縛をコメントアウトしてください。

-        $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);
+        // $this->bind()->annotatedWith(Message::class)->toInstance('Hello %s!' . PHP_EOL);

これではRay.Diは#[Message]とアトリビュートされた依存に何を注入すれば良いかわかりません。

実行すると以下のようなエラーが出力されます。

PHP Fatal error:  Uncaught exception 'Ray\Di\Exception\Unbound' with message '-Ray\Tutorial\Message'
- dependency '' with name 'Ray\Tutorial\Message' used in /tmp/tutorial/src/IntlPrinter.php:8 ($message)
- dependency 'Ray\Tutorial\PrinterInterface' with name '' /tmp/tutorial/src/CleanGreeter.php:6 ($printer)

これはIntlPrinter.php:8$messageが依存解決できないので、それに依存するCleanGreeter.php:6$printerも依存解決できなくて注入が失敗しましたというエラーです。このように依存の依存が解決できない時はその依存のネストも表示されます。

最後に、以下のような束縛をsrc/SpanishModule.phpとして作成してTestModuleと同じように上書きしてみましょう。

<?php
namespace Ray\Tutorial;

use Ray\Di\AbstractModule;

class SpanishModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind()->annotatedWith(Message::class)->toInstance('¡Hola %s!' . PHP_EOL);
    }
}
use Ray\Tutorial\AppModule;
-use Ray\Tutorial\TestModule;
+use Ray\Tutorial\SpanishModule;
use Ray\Tutorial\GreeterInterface;

require dirname(__DIR__) . '/vendor/autoload.php';

$module = new AppModule();
-$module->override(new TestModule());
+$module->override(new SpanishModule());

以下のようにスペイン語の挨拶に変わりましたか?

¡Hola DI!
¡Hola AOP!
¡Hola REST!

まとめ

DIパターンとRay.Diの基本を見てきました。 依存はユーザーコードが内部から取得するのではなく外部から再起的に注入され、オブジェクトグラフが生成されます。

コンパイルタイムで依存の束縛を行い、ランタイムでは与えられた依存をインターフェースを用いて実行します。DIパターンに従う事でSRP原則1やDIP原則2が自然に守られるようになりました。

コードから依存を確保する責務が取り除かれ疎結合でシンプルになりました。コードは安定しながら柔軟で、拡張に対しては開いていても修正に対しては閉じています。4


  1. 単一責任原則 (SRP)  2

  2. 依存性逆転の原則 (DIP)  2

  3. “コンピュータサイエンスにおいて、オブジェクト指向のアプリケーションは相互に関係のある複雑なオブジェクト網を持ちます。オブジェクトはあるオブジェクトから所有されているか、他のオブジェクト(またはそのリファレンス)を含んでいるか、そのどちらかでお互いに接続されています。このオブジェクト網をオブジェクトグラフと呼びます。” Object Graph 

  4. 開放/閉鎖原則 (OCP)