最近は Laravel + AngularJS で Web アプリケーションを開発する毎日です。
少し時間が取れたので Laravel フレームワークのソースを読んでいます。その中から Laravel の肝でもある IoC コンテナの使い方をまとめてみました。
Laravel の IoC コンテナ
Laravel で使われている IoC コンテナは、Illiminate\Foundation\Application
クラスです。これはIlliminate\Container\Container
クラスを継承したもので、コンテナとしての基本機能はIlliminate\Container\Container
クラスが担っています。
Laravel アプリケーションで、この IoC コンテナを利用する際は、App
クラスというIlliminate\Foundation\Application
のファサードクラスが用意されているので、こちらを利用することが多いです。
// Foo インスタンスを取得 $foo = App::make('Foo');
App
クラスのgetFacadeAccessor()
メソッドを使うと、Illiminate\Foundation\Application
クラスのインスタンスを取得することができるので、直接メソッドを実行できます。
// Foo インスタンスを取得 $foo = App::getFacadeAccessor()->make('Foo');
下準備
ここでは、IoC コンテナの動きを見ていくので、Laravel フレームワークを起動せずにシンプルなコードを書きます。
ファサードクラスは定義されていないので、Illiminate\Foundation\Application
クラスのインスタンスを直接操作します。(App
クラスを使う場合は、メソッド呼び出しをクラスメソッドとして読み替えて下さい。)
<?php require __DIR__.'/vendor/autoload.php'; class Foo { /** * @var string */ protected $message; /** * @var string $message */ public function __construct($message = '') { $this->message = $message; } /** * @return string */ public function getMessage() { return $this->message; } } $app = new \Illuminate\Foundation\Application();
コンテナからインスタンス取得
IoC コンテナからインスタンスを取得するには、make()
メソッドを使います。引数には、コンテナにバインドした際の名前を指定します。コンテナに指定した名前がバインドされていない場合、クラスとしてインスタンスを生成します。下記では、’Foo’という名前のバインドは行っていませんが、Fooクラスのインスタンスを返します。make()
メソッドの第2引数に連想配列を渡すと、生成するインスタンスのコンストラクタに引数として渡されます。
$foo = $app->make('Foo'); // message = '' $foo = $app->make('Foo', ['Hello']); // message = 'Hello'
IoC コンテナは、AraryAccess
インターフェースを実装しているので、配列としてアクセスすると、内部でmake()
メソッドを実行して、該当インスタンスを返します。
$foo = $app['Foo']; // message = ''
コンテナにファクトリをバインド
コンテナにインスタンスを生成するファクトリをバインドしてみます。バインドを行うメソッドはいくつかあり、それぞれ用途に応じて使い分けます。
bind()
インスタンスを生成するファクトリをバインドします。make()
メソッド実行毎にファクトリが実行され、インスタンスが生成されます。
$app->bind('foo_bind', 'Foo'); $foo = $app->make('foo_bind');
$app->bind('foo_bind', function($app) { return new Foo('foo_bind'); }); $foo = $app->make('foo_bind');
bindShared()
bind()
と同じですが、make()
実行時に生成されたインスタンスをコンテナが保持します。再度make()
を実行した際は同じインスタンスを返します。下記コードでは、$foo1
と$foo2
は同じインスタンスです。
$app->bindShared('foo_bind_shared', function($app) { return new Foo('foo_bind_shared'); }); $foo1 = $app->make('foo_bind_shared'); $foo2 = $app->make('foo_bind_shared');
bindIf()
コンテナに同じ名前のバインドが無ければ、バインドします。すでにあれば何も行いません。
$app->bindIf('foo_bind', function($app) { return new Foo('foo_bind_if'); }); $foo = $app->make('foo_bind'); // 変わっていない
instance()
生成したインスタンスをバインドします。make()
メソッド実行時は、常にこのインスタンスを返します。
$app->instance('foo_instance', new Foo('instance')); $foo1 = $app->make('foo_instance'); // message == 'instance' $foo2 = $app->make('foo_instance'); // message == 'instance'
singleton()
bindShared()と同じくmake()
実行時に生成したインスタンスをコンテナが保持するので、以後同じインスタンスを返します。
$app->singleton('foo_singleton', 'Foo'); $foo1 = $app->make('foo_singleton'); // message == 'foo_singleton' $foo2 = $app->make('foo_singleton'); // message == 'foo_singleton' assert($foo1 === $foo2);
$app->singleton('foo_singleton_closure', function($app) { return new Foo('foo_singleton_closure'); }); $foo1 = $app->make('foo_singleton_clusure'); // message == 'foo_singleton_closure' $foo2 = $app->make('foo_singleton_closure'); // message == 'foo_singleton_closure' assert($foo1 === $foo2);
alias()
コンテナにバインドした名前に別名を付けます。
$app->share('bar', 'foo'); $bar = $app->make('bar'); // message == 'foo'
extend()
コンテナにバインドされたファクトリを拡張します。クロージャの第1引数に元にバインドされたファクトリが返すインスタンスが渡されるので、追加処理を記述して、そのインスタンスを返すようにします。
$app->extend('foo_bind', function($foo, $app) { $foo->extend = 'ex'; return $foo; }); $foo = $app->make('foo_bind'); // message == 'foo_bind', extend = 'ex'
連想配列として代入
make()
メソッドと同じく連想配列として代入することでバインドすることができます。この場合、bind()
メソッドの実行と同じ扱いになります。
$app['foo_set'] = function() { return Foo('foo_set'); }; $foo = $app->make('foo_set');
具象クラスをインジェクトするDI
では IoC コンテナを使った DI を行ってみます。サンプルのソースは以下です。
このソースでは、HelloクラスとBarクラスがあり、Barクラスではコンストラクタで、Helloクラスのインスタンスを必要としています。
IoC コンテナのmake()
メソッドを使って Bar クラスのインスタンスを取得しようとすると、コンテナがリフレクションを使って、コンストラクタの引数を判別して、Helloクラスのインスタンを生成し、Barクラスのコンストラクタに渡してくれます。
結果として、Helloクラスのインスタンスを保持したBarクラスのインスタンスを取得することができます。
このように具象クラスであればコンテナへのバインドを行わなくても、よしなにインスタンスをインジェクトしてくれます。
インターフェースをインジェクトするDI
次は、インスタンスを生成するファクトリをコンテナにバインドしてDIを行います。
サンプルソースを変更しました。Greetableインターフェースを定義して、Helloクラスはこのインターフェースをimplementしています。Barクラスのコンストラクタでは、Helloクラスではなく、Greetableインターフェースのインスタンスを引数として取るように変更しました。
Barクラスを$app->make('bar')
で取得しようとするとIlluminate\Container\BindingResolutionException
という例外が発生します。これはコンストラクタで必要としているGreetable
はインターフェースのため、インスタンス化ができないためです。
これを解決するにはいくつかの方法があります。
Greetable
という名前で取得できるインスタンスをコンテナにバインドしておきます。ここではHello
クラスを指定しています。これにより、BarクラスのコンストラクタにはHelloクラスのインスタンスがセットされるようになります。
$app->bind('Greetable', 'Hello'); $app->make('Bar')->say();
Greetable
という名前にHelloクラスのインスタンスを生成するファクトリを指定することもできます。
$app->bind('Greetable', function($app) { return new Hello(); }); $app->make('Bar')->say();
もちろん、Bar
という名前でBarクラスのファクトリをバインドする方法もokです。
$app->bind('Bar', function($app) { return new Bar($app->make('Hello')); }); $app->make('Bar')->say();
インジェクトするクラスを変える
IoC コンテナを使って、DI できるようにしておくと、インジェクトするクラスを簡単に変更することができます。
Helloクラスの代わりにGreetable
を実装したByeクラスをインジェクトするには、下記のようにコンテナのバインドを変更するだけです。Barクラスの実装は変更する必要がありません。
class Bye implements Greetable { /** * */ public function greet() { echo 'Bye'; } } $app->bind('Greetable', 'Bye'); $app->make('Bar')->say(); // Bye
おわりに
Laravel に実装されている IoC コンテナの使い方を見てみました。
IoC コンテナは、Laravelを構成する重要な機能なので、使い方を理解しておくとフレームワークへの理解も深まります。
コンテナを連想配列ライクに利用すると Pimple と似ているので、Pimple を使ったことがある人には馴染みやすいかもしれませんね。
(Laravel 作者Taylor Otwell氏の著書(Laravel: From Apprentice To Artisanでは「Laravel の IoC コンテナは、のPimple IoC コンテナとそのまま置き 換えることができます。」と書かれていたりします:D)