Ray.Di を知っていますか?
私は普段の開発ではSymfonyを利用しているので、Symfonyの DependencyInjection 機能を使っていますが、最近Symfonyフレームワークを使うほどでない小さなcliアプリで実際に使ってみました。初めて使う場合に戸惑うところ・引っかかったところがあるので、使い方をまとめました。

Ray.Diとは

Javaの Guice というDIフレームワークの主要な機能を網羅したPHPのDIフレームワークです。
PHPフレームワークの BEAR.Sunday で使われていますが、単独でも利用できます。
BEAR.Sundayで使う場合は専用のアノテーションを使うことが多いのですが、アノテーションを使わずにPHPコードだけでも設定できるので、サードパーティのクラスを使う場合でも問題なく利用することができます。

Ray.Diを使う

早速使ってみましょう。

例として時間帯によって違う挨拶を返すcliアプリを作ってみることにします。
環境変数 lang によって言語を切り替える機能がついています。

# 朝9時に実行
$ php scripts/run.php 77web
Good morning, 77web.

DIを使わずに書いてみると、アプリケーションの入口になる scripts/run.php が依存関係のコードで雑然としていますね。
https://github.com/77web/ray-di-sample/tree/no-di/scripts/run.php

#!/usr/bin/env php
<?php

use PHPMentors\DomainCommons\DateTime\Clock;
use Psr\Log\NullLogger;
use Quartetcom\RayDiSample\Command\GreetingCommand;
use Quartetcom\RayDiSample\Greeter;
use Quartetcom\RayDiSample\GreetingFormatter;
use Quartetcom\RayDiSample\GreetingRegistry;
use Quartetcom\RayDiSample\TimeDetector;
use Symfony\Component\Console\Application;
use Symfony\Component\Yaml\Yaml;

require __DIR__.'/../vendor/autoload.php';

ini_set('date.timezone', 'Asia/Tokyo');
if (!getenv('lang')) {
    putenv('lang=de');
}

$timeDetector = TimeDetector::createFromConfig([
    'morning' => range(4, 10),
    'day' => range(11, 15),
    'night' => array_merge(range(0, 3), range(16, 23)),
]);
$greetings = Yaml::parseFile(__DIR__.'/../src/Resources/config/greetings.yml')['greetings'];
$lang = getenv('lang');
$greetRegistry = new GreetingRegistry($greetings[$lang]);
$greeter = new Greeter($greetRegistry, $timeDetector, new GreetingFormatter('%greeting%, %name%.'), new Clock());
$command = new GreetingCommand($greeter, new NullLogger());

$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

Ray.Diを使って書き直していきます。

composerでray/diを入れる

composerで追加します。

$ composer require ray/di

Moduleクラスを作る

DI設定をするためのModuleクラスを作ります。
Ray\Di\AbstractModule クラスのサブクラスにする必要があります。

<?php
namespace Quartetcom\RayDiSample\DependencyInjection;

use Ray\Di\AbstractModule;

class GreeterModule extends AbstractModule
{
}

DIを設定する

いよいよ GreeterModule に実際にDIの設定を書いていきましょう。
AbstractModule には抽象メソッドとして configure() メソッドが定義されており、実際のDI設定は GreeterModule::configure() メソッドに記述します。

<?php
// ...

class GreeterModule extends AbstractModule
{
+    public function configure()
+    {
+    }
}

コンストラクタ引数がない、インターフェイスで指定されたクラスの設定 to()

一番基本の書き方です。
bind() にインターフェイス名、 to() に実際に利用する具象クラス名を指定します。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(LoggerInterface::class)->to(NullLogger::class);
    }
}

コンストラクタ引数がない具象クラスの設定 bind()

bind() にクラス名を指定します。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(Clock::class);
    }
}

コンストラクタで他のクラスのインスタンスだけを注入される具象クラスの設定 bind()

依存クラスをすべて設定した上で、 bind() にクラス名を指定します。
設定を記述する順序は自由なので、前後しても大丈夫です。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(LoggerInterface::class)->to(NullLogger::class);
+        $this->bind(GreetingCommand::class); // GreeterとLoggerInterfaceに依存
+        $this->bind(Greeter::class); // 依存対象を後に書いても良い
    }
}

独自のファクトリが存在するクラス toInstance()

toInstance() を使います。
bind() にクラス名を指定し、 toInstance() にファクトリで作成した実際のクラスのインスタンスを指定します。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(TimeDetector::class)->toInstance(TimeDetector::createFromConfig([
+            'morning' => range(4, 10),
+            'day' => range(11, 15),
+            'night' => array_merge(range(0, 3), range(16, 23)),
+        ]));
    }
}

コンストラクタでパラメータ(クラスでなく単なる値)を注入されるクラスの設定 toConstructor()

bind()toConstructor() の両方にクラス名を指定し、 toConstructor() の第2引数でコンストラクタ引数の変数名と仮想クラス名(パラメータ名)の対応を指定します。
実際に渡したい値は、 annotatedWith() に仮想クラス名を指定し、さきほどの toInstance() を使って設定します。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(GreetingFormatter::class)->toConstructor(GreetingFormatter::class, [
+            'format' => 'greeting_formatter_format',
+        ]);
+        $this->bind()->annotatedWith('greeting_formatter_format')->toInstance('%greeting%, %name%.');
    }
}

使うインスタンスを切り替える

toProvider() を使います。 まず、Ray\Di\ProviderInterface を実装した GreetingRegistryProvider クラスを追加します。
Providerは、引数なしの get() メソッドから目的のクラスのインスタンスを返すように実装します。

<?php

namespace Quartetcom\RayDiSample\DependencyInjection;

use Quartetcom\RayDiSample\GreetingRegistry;
use Ray\Di\Di\Named;
use Ray\Di\ProviderInterface;
use Ray\Di\SetContextInterface;

class GreetingRegistryProvider implements ProviderInterface, SetContextInterface
{
    /**
     * @var array
     */
    private $greetings;

    /**
     * @var string
     */
    private $context;

    /**
     * Providerクラスに何かの値をDIしたい場合は@Namedアノテーションが必要
     *
     * @Named("greetings=greeting_registry_greetings")
     * @param array $greetings
     */
    public function __construct(array $greetings)
    {
        $this->greetings = $greetings;
    }

    /**
     * @param string $context
     */
    public function setContext($context)
    {
        $this->context = $context;
    }

    /**
     * @return GreetingRegistry
     */
    public function get()
    {
        return new GreetingRegistry($this->greetings[$this->context]);
    }
}

注意すべきなのはProviderクラスに何かの値をDIしたい場合は @Named アノテーションを使う必要があるということです。 @Named アノテーションは通常、同じクラスのインスタンスを複数使用する場合などにDI対象を特定するために使うものです。通常のクラスなら annotatedWith() を用いてModule側で指定可能ですが、Providerには @Named を使って指定しないといけないようです。
ソースコードにDIのための概念が入ってしまいますが、ProviderはModuleと同様にRay.Diを使うのをやめた場合は使わないクラスなので問題ないでしょう。

Providerクラスができたら、GreeterModuleでは bind() にクラス名を指定し、 プロバイダークラス名を toProvider() に指定します。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
+        $this->bind(GreetingRegistry::class)->toProvider(GreetingRegistryProvider::class, getenv('lang')); // getenv('lang')はGreetingRegistryProvider::$contextになる
+        $this->bind()->annotatedWith('greeting_registry_greetings')->toInstance(Yaml::parseFile(__DIR__.'/../Resources/config/greetings.yml')['greetings']);
    }
}

注入されたクラスを実際に使う

最終的な GreeterModule::configure() は下記のようになりました。

<?php
// ...

class GreeterModule extends AbstractModule
{
    public function configure()
    {
        $this->bind(TimeDetector::class)->toInstance(TimeDetector::createFromConfig([
            'morning' => range(4, 10),
            'day' => range(11, 15),
            'night' => array_merge(range(0, 3), range(16, 23)),
        ]));
        $this->bind(Clock::class);
        $this->bind(Greeter::class);
        $this->bind(GreetingFormatter::class)->toConstructor(GreetingFormatter::class, [
            'format' => 'greeting_formatter_format',
        ]);
        $this->bind()->annotatedWith('greeting_formatter_format')->toInstance('%greeting%, %name%.');
        $this->bind(GreetingRegistry::class)->toProvider(GreetingRegistryProvider::class, getenv('lang'));
        $this->bind()->annotatedWith('greeting_registry_greetings')->toInstance(Yaml::parseFile(__DIR__.'/../Resources/config/greetings.yml')['greetings']);
        $this->bind(LoggerInterface::class)->to(NullLogger::class);
        $this->bind(GreetingCommand::class);
    }
}

では、 scripts/run.php で、DI設定に従って GreetingCommand のインスタンスを取得してみましょう。

<?php

// ...

-$timeDetector = TimeDetector::createFromConfig([
-    'morning' => range(4, 10),
-    'day' => range(11, 15),
-    'night' => array_merge(range(0, 3), range(16, 23)),
-]);
-$greetings = Yaml::parseFile(__DIR__.'/../src/Resources/config/greetings.yml')['greetings'];
-$lang = getenv('lang');
-$greetRegistry = new GreetingRegistry($greetings[$lang]);
-$greeter = new Greeter($greetRegistry, $timeDetector, new GreetingFormatter('%greeting%, %name%.'), new Clock());
-$command = new GreetingCommand($greeter, new NullLogger());

+/** @var GreeterCommand $command */
+$command = new Injector(new GreeterModule())->getInstance(GreetCommand::class);

// ...

DIなし に比べて Ray.Diを使った版 はスッキリしましたね!
編集したのは 3ファイルだけ なので簡単に導入できました。

まとめ

Ray.Diを使うことで scripts/run.php での処理の流れは圧倒的に追いやすくなりました。
ライブラリとして実装したクラスに全くDI専用のアノテーションを書かなくても使えるので、Ray.Di用のModule(とProvider)を追加するだけで気軽に試すことができますし、他のDIフレームワークへの切り替えも楽にできそうです。