6 フォロワー

依存性注入コンテナ

依存性注入(DI)コンテナは、オブジェクトとそのすべての依存オブジェクトをインスタンス化および構成する方法を知っているオブジェクトです。 Martin Fowlerの記事では、DIコンテナがなぜ有用であるかが詳しく説明されています。ここでは主に、Yiiが提供するDIコンテナの使用法について説明します。

依存性注入

Yiiは、yii\di\Containerクラスを通じてDIコンテナ機能を提供します。次の種類の依存性注入をサポートしています。

  • コンストラクタ注入;
  • メソッド注入;
  • セッターとプロパティ注入;
  • PHP callable注入;

コンストラクタ注入

DIコンテナは、コンストラクタパラメータの型ヒントを利用してコンストラクタ注入をサポートします。型ヒントは、新しいオブジェクトを作成する際に、どのクラスまたはインターフェースが依存しているかをコンテナに伝えます。コンテナは、依存クラスまたはインターフェースのインスタンスを取得しようとし、コンストラクタを通じて新しいオブジェクトに注入します。例えば、

class Foo
{
    public function __construct(Bar $bar)
    {
    }
}

$foo = $container->get('Foo');
// which is equivalent to the following:
$bar = new Bar;
$foo = new Foo($bar);

メソッド注入

通常、クラスの依存関係はコンストラクタに渡され、クラスのライフサイクル全体で使用できます。メソッド注入を使用すると、クラスの単一のメソッドでのみ必要な依存関係を提供することが可能になり、コンストラクタに渡すことが不可能であったり、ほとんどのユースケースで過度のオーバーヘッドが発生する可能性があります。

クラスメソッドは、次の例のdoSomething()メソッドのように定義できます。

class MyClass extends \yii\base\Component
{
    public function __construct(/*Some lightweight dependencies here*/, $config = [])
    {
        // ...
    }

    public function doSomething($param1, \my\heavy\Dependency $something)
    {
        // do something with $something
    }
}

そのメソッドを、\my\heavy\Dependencyのインスタンスを自分で渡すか、次の例のようにyii\di\Container::invoke()を使用して呼び出すことができます。

$obj = new MyClass(/*...*/);
Yii::$container->invoke([$obj, 'doSomething'], ['param1' => 42]); // $something will be provided by the DI container

セッターとプロパティ注入

セッターとプロパティ注入は、構成を通じてサポートされます。依存関係を登録するとき、または新しいオブジェクトを作成するときに、対応するセッターまたはプロパティを通じて依存関係を注入するためにコンテナによって使用される構成を指定できます。例えば、

use yii\base\BaseObject;

class Foo extends BaseObject
{
    public $bar;

    private $_qux;

    public function getQux()
    {
        return $this->_qux;
    }

    public function setQux(Qux $qux)
    {
        $this->_qux = $qux;
    }
}

$container->get('Foo', [], [
    'bar' => $container->get('Bar'),
    'qux' => $container->get('Qux'),
]);

情報: yii\di\Container::get() メソッドは、3番目のパラメータとして、作成されるオブジェクトに適用されるべき設定配列を受け取ります。クラスが yii\base\Configurable インターフェース(例えば yii\base\BaseObject)を実装している場合、設定配列はクラスのコンストラクタの最後のパラメータとして渡されます。そうでない場合、設定はオブジェクトが作成されたに適用されます。

PHP 呼び出し可能オブジェクトのインジェクション

この場合、コンテナは登録された PHP 呼び出し可能オブジェクトを使ってクラスの新しいインスタンスを構築します。yii\di\Container::get() が呼び出されるたびに、対応する呼び出し可能オブジェクトが呼び出されます。呼び出し可能オブジェクトは、依存関係を解決し、新しく作成されたオブジェクトに適切に注入する責任があります。例えば、

$container->set('Foo', function ($container, $params, $config) {
    $foo = new Foo(new Bar);
    // ... other initializations ...
    return $foo;
});

$foo = $container->get('Foo');

新しいオブジェクトを構築する複雑なロジックを隠すために、静的クラスメソッドを呼び出し可能オブジェクトとして使用できます。例えば、

class FooBuilder
{
    public static function build($container, $params, $config)
    {
        $foo = new Foo(new Bar);
        // ... other initializations ...
        return $foo;
    }
}

$container->set('Foo', ['app\helper\FooBuilder', 'build']);

$foo = $container->get('Foo');

そうすることで、Foo クラスを設定したい人は、それがどのように構築されるかを意識する必要がなくなります。

依存関係の登録

yii\di\Container::set() を使って依存関係を登録できます。登録には、依存関係の名前と依存関係の定義が必要です。依存関係の名前は、クラス名、インターフェース名、またはエイリアス名にすることができます。依存関係の定義は、クラス名、設定配列、または PHP 呼び出し可能オブジェクトにすることができます。

$container = new \yii\di\Container;

// register a class name as is. This can be skipped.
$container->set('yii\db\Connection');

// register an interface
// When a class depends on the interface, the corresponding class
// will be instantiated as the dependent object
$container->set('yii\mail\MailInterface', 'yii\symfonymailer\Mailer');

// register an alias name. You can use $container->get('foo')
// to create an instance of Connection
$container->set('foo', 'yii\db\Connection');

// register an alias with `Instance::of`
$container->set('bar', Instance::of('foo'));

// register a class with configuration. The configuration
// will be applied when the class is instantiated by get()
$container->set('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register an alias name with class configuration
// In this case, a "class" or "__class" element is required to specify the class
$container->set('db', [
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

// register callable closure or array
// The callable will be executed each time when $container->get('db') is called
$container->set('db', function ($container, $params, $config) {
    return new \yii\db\Connection($config);
});
$container->set('db', ['app\db\DbFactory', 'create']);

// register a component instance
// $container->get('pageCache') will return the same instance each time it is called
$container->set('pageCache', new FileCache);

ヒント: 依存関係の名前が対応する依存関係の定義と同じ場合は、DIコンテナに登録する必要はありません。

set() を介して登録された依存関係は、依存関係が必要になるたびにインスタンスを生成します。 yii\di\Container::setSingleton() を使用して、単一のインスタンスのみを生成する依存関係を登録できます。

$container->setSingleton('yii\db\Connection', [
    'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

依存関係の解決

依存関係を登録したら、DIコンテナを使用して新しいオブジェクトを作成できます。コンテナは、依存関係を自動的に解決し、新しく作成されたオブジェクトに注入します。依存関係の解決は再帰的です。つまり、依存関係に別の依存関係がある場合、それらの依存関係も自動的に解決されます。

get() を使用して、オブジェクトインスタンスを作成または取得できます。このメソッドは、クラス名、インターフェース名、またはエイリアス名にできる依存関係の名前を受け取ります。依存関係の名前は、set() または setSingleton() を介して登録される場合があります。オプションで、クラスコンストラクタパラメータのリストと、新しく作成されたオブジェクトを設定するための 設定 を指定できます。

例:

// "db" is a previously registered alias name
$db = $container->get('db');

// equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]);
$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]);

// equivalent to: $api = new \app\components\Api($host, $apiKey);
$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]);

舞台裏では、DIコンテナは新しいオブジェクトを作成する以上の多くの作業を行います。コンテナは最初にクラスのコンストラクタを検査して、依存するクラスまたはインターフェースの名前を調べ、それらの依存関係を再帰的に自動的に解決します。

次のコードは、より洗練された例を示しています。UserLister クラスは、UserFinderInterface インターフェースを実装するオブジェクトに依存しています。UserFinder クラスはこのインターフェースを実装し、Connection オブジェクトに依存しています。これらの依存関係はすべて、クラスコンストラクタパラメータの型ヒントを通じて宣言されます。適切な依存関係の登録により、DIコンテナはこれらの依存関係を自動的に解決し、get('userLister') の簡単な呼び出しで新しい UserLister インスタンスを作成できます。

namespace app\models;

use yii\base\BaseObject;
use yii\db\Connection;
use yii\di\Container;

interface UserFinderInterface
{
    function findUser();
}

class UserFinder extends BaseObject implements UserFinderInterface
{
    public $db;

    public function __construct(Connection $db, $config = [])
    {
        $this->db = $db;
        parent::__construct($config);
    }

    public function findUser()
    {
    }
}

class UserLister extends BaseObject
{
    public $finder;

    public function __construct(UserFinderInterface $finder, $config = [])
    {
        $this->finder = $finder;
        parent::__construct($config);
    }
}

$container = new Container;
$container->set('yii\db\Connection', [
    'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
    'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');

$lister = $container->get('userLister');

// which is equivalent to:

$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);

実践的な使い方

Yii は、アプリケーションの エントリスクリプトYii.php ファイルを含めると、DIコンテナを作成します。DIコンテナは Yii::$container を介してアクセスできます。Yii::createObject() を呼び出すと、メソッドは実際にはコンテナの get() メソッドを呼び出して新しいオブジェクトを作成します。前述のように、DIコンテナは依存関係(存在する場合)を自動的に解決し、取得したオブジェクトに注入します。Yii は、ほとんどのコアコードで Yii::createObject() を使用して新しいオブジェクトを作成するため、これは Yii::$container を処理することでオブジェクトをグローバルにカスタマイズできることを意味します。

たとえば、yii\widgets\LinkPager のデフォルトのページネーションボタンの数をグローバルにカスタマイズしてみましょう。

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

ここで、次のコードを使用してビューでウィジェットを使用すると、maxButtonCount プロパティは、クラスで定義されているデフォルト値 10 ではなく、5 として初期化されます。

echo \yii\widgets\LinkPager::widget();

ただし、DIコンテナを介して設定された値をオーバーライドすることもできます。

echo \yii\widgets\LinkPager::widget(['maxButtonCount' => 20]);

注: ウィジェットの呼び出しで指定されたプロパティは、常にDIコンテナの定義をオーバーライドします。たとえば、'options' => ['id' => 'mypager'] のように配列を指定した場合でも、これらは他のオプションとマージされるのではなく、それらを置き換えます。

別の例として、DIコンテナの自動コンストラクタインジェクションを利用することです。コントローラクラスが、ホテル予約サービスなどの他のオブジェクトに依存していると仮定します。コンストラクタパラメータを通じて依存関係を宣言し、DIコンテナに解決させることができます。

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{
    protected $bookingService;

    public function __construct($id, $module, BookingInterface $bookingService, $config = [])
    {
        $this->bookingService = $bookingService;
        parent::__construct($id, $module, $config);
    }
}

ブラウザからこのコントローラにアクセスすると、BookingInterface をインスタンス化できないというエラーが表示されます。これは、DIコンテナにこの依存関係を処理する方法を指示する必要があるためです。

\Yii::$container->set('app\components\BookingInterface', 'app\components\BookingService');

もう一度コントローラにアクセスすると、app\components\BookingService のインスタンスが作成され、コントローラのコンストラクタの3番目のパラメータとして注入されます。

PHP 7 を使用する場合、Yii 2.0.36 以降では、Web コントローラとコンソールコントローラの両方でアクションインジェクションが利用可能です。

namespace app\controllers;

use yii\web\Controller;
use app\components\BookingInterface;

class HotelController extends Controller
{    
    public function actionBook($id, BookingInterface $bookingService)
    {
        $result = $bookingService->book($id);
        // ...    
    }
}

高度な実践的な使い方

API アプリケーションに取り組んでいて、以下があるとしましょう。

  • yii\web\Request を拡張し、追加機能を提供する app\components\Request クラス
  • yii\web\Response を拡張し、作成時に format プロパティを json に設定する必要がある app\components\Response クラス
  • いくつかのファイルストレージにあるドキュメントの操作に関するロジックを実装する app\storage\FileStorage および app\storage\DocumentsReader クラス

    class FileStorage
    {
        public function __construct($root) {
            // whatever
        }
    }
      
    class DocumentsReader
    {
        public function __construct(FileStorage $fs) {
            // whatever
        }
    }
    

構成配列を setDefinitions() または setSingletons() メソッドに渡すことで、複数の定義を一度に構成できます。構成配列を反復処理すると、メソッドは各項目に対して set() または setSingleton() をそれぞれ呼び出します。

構成配列の形式は次のとおりです。

  • key: クラス名、インターフェース名、またはエイリアス名。キーは set() メソッドに最初の引数 $class として渡されます。
  • value: $class に関連付けられた定義。可能な値は、set() ドキュメントの $definition パラメータで説明されています。 set() メソッドに2番目の引数 $definition として渡されます。

たとえば、上記の要件に従うようにコンテナを構成してみましょう。

$container->setDefinitions([
    'yii\web\Request' => 'app\components\Request',
    'yii\web\Response' => [
        'class' => 'app\components\Response',
        'format' => 'json'
    ],
    'app\storage\DocumentsReader' => function ($container, $params, $config) {
        $fs = new app\storage\FileStorage('/var/tempfiles');
        return new app\storage\DocumentsReader($fs);
    }
]);

$reader = $container->get('app\storage\DocumentsReader'); 
// Will create DocumentReader object with its dependencies as described in the config 

ヒント: コンテナは、バージョン 2.0.11 以降、アプリケーション構成を使用して宣言的に構成できます。 構成ガイド記事の アプリケーション構成 サブセクションを確認してください。

すべて正常に機能しますが、DocumentWriter クラスを作成する必要がある場合は、FileStorage オブジェクトを作成する行をコピーアンドペーストする必要があります。これは明らかに賢い方法ではありません。

依存関係の解決 サブセクションで説明したように、set() および setSingleton() は、オプションで3番目の引数として依存関係のコンストラクタパラメータを受け取ることができます。コンストラクタパラメータを設定するには、__construct() オプションを使用できます。

例を変更してみましょう。

$container->setDefinitions([
    'tempFileStorage' => [ // we've created an alias for convenience
        'class' => 'app\storage\FileStorage',
        '__construct()' => ['/var/tempfiles'], // could be extracted from some config files
    ],
    'app\storage\DocumentsReader' => [
        'class' => 'app\storage\DocumentsReader',
        '__construct()' => [Instance::of('tempFileStorage')],
    ],
    'app\storage\DocumentsWriter' => [
        'class' => 'app\storage\DocumentsWriter',
        '__construct()' => [Instance::of('tempFileStorage')]
    ]
]);

$reader = $container->get('app\storage\DocumentsReader'); 
// Will behave exactly the same as in the previous example.

Instance::of('tempFileStorage') という表記に気付くかもしれません。これは、ContainertempFileStorage という名前で登録された依存関係を暗黙的に提供し、app\storage\DocumentsWriter コンストラクタの最初の引数として渡すことを意味します。

注: setDefinitions() および setSingletons() メソッドは、バージョン 2.0.11 以降で使用可能です。

構成の最適化におけるもう1つのステップは、いくつかの依存関係をシングルトンとして登録することです。 set() を介して登録された依存関係は、必要なときに毎回インスタンス化されます。一部のクラスは実行時に状態を変更しないため、アプリケーションのパフォーマンスを向上させるためにシングルトンとして登録できます。

良い例は、単純なAPI (例: $fs->read()$fs->write()) でファイルシステムに対していくつかの操作を実行する app\storage\FileStorage クラスです。これらの操作はクラスの内部状態を変更しないため、インスタンスを1回作成して複数回使用できます。

$container->setSingletons([
    'tempFileStorage' => [
        'class' => 'app\storage\FileStorage',
        '__construct()' => ['/var/tempfiles']
    ],
]);

$container->setDefinitions([
    'app\storage\DocumentsReader' => [
        'class' => 'app\storage\DocumentsReader',
        '__construct()' => [Instance::of('tempFileStorage')],
    ],
    'app\storage\DocumentsWriter' => [
        'class' => 'app\storage\DocumentsWriter',
        '__construct()' => [Instance::of('tempFileStorage')],
    ]
]);

$reader = $container->get('app\storage\DocumentsReader');

依存関係を登録するタイミング

新しいオブジェクトが作成されるときに依存関係が必要になるため、それらの登録はできるだけ早く行う必要があります。以下は推奨されるプラクティスです。

  • アプリケーションの開発者である場合は、アプリケーション構成を使用して依存関係を登録できます。 構成ガイド記事の アプリケーション構成 サブセクションをお読みください。
  • 再配布可能な 拡張機能 の開発者である場合は、拡張機能のブートストラップクラスで依存関係を登録できます。

まとめ

依存性注入と サービスロケータ は、疎結合でテストしやすい方法でソフトウェアを構築できる一般的なデザインパターンです。依存性注入とサービスロケータについての理解を深めるために、Martin's article を読むことを強くお勧めします。

Yii は、依存性注入(DI)コンテナの上に サービスロケータ を実装します。サービスロケータが新しいオブジェクトインスタンスを作成しようとすると、呼び出しをDIコンテナに転送します。後者は、上記のように依存関係を自動的に解決します。

タイプミスを見つけたり、このページを改善する必要があると思われる場合は、
github で編集 !