はじめに
サービスコンテナは、
Laravelのコアとなる機能で
Laravelのめちゃくちゃ便利で魔法のような仕組みを
実現してくれているものです。
しかし、普段Laravelを使っていても
簡単なアプリケーションを作るうえではあまり意識しないところかもしれません。
(意識しなくていいようになってる)
このサービスコンテナについて理解すると、
より便利に自分でカスタマイズしたりできるし、
何より面白いです。
Laravelのサービスコンテナとは何なのかについて
調べましたので、簡単に解説します。
サービスコンテナとは
サービスコンテナとは、
一言でいうと
「クラスをインスタンス化してくれるマシーン」
というイメージ。
具体的なコードで見てみます。
ClassAをインスタンス化するとき、
普通はこのようにnewしますね。
class ClassA
{
//
}
$classA = new ClassA();
サービスコンテナを使ってインスタンス化する場合はこのようになります。
$classA = app()->make(ClassA::class);
このapp()
関数でサービスコンテナを取得し、
そのサービスコンテナのmake()
メソッドでインスタンス化しています。
$classA
に入っているのは
先ほどnewしたときと全く同じです。
ただClassAをインスタンス化してくれました。
「newと同じならそんな機能いらなくね?」
と思ったかもしれませんが、
もちろんそれだけではありません。
サービスコンテナは、
ある2つの強力な武器(便利機能)を持った
「クラスをインスタンス化してくれるマシーン」
です。
その2つの便利機能とは
【1】依存解決してくれる
【2】インスタンス化の方法をカスタマイズできる
です。
この2つの武器がどういうものなのか、
見ていきます。
【1】依存解決
「依存解決」っていったい何なのかというと、
「コンストラクタで型宣言(タイプヒンティング)したクラスを、自動でインスタンス化して渡してくれる」
ということです。
具体例をコードで見てみます。
ClassAとClassBがあり、
ClassAのコンストラクタでClassBを型宣言して受け取るようになっています。
class ClassA
{
public function __construct(ClassB $classB)
{
\Log::info('ClassAインスタンス化完了');
}
}
class ClassB
{
public function __construct()
{
\Log::info('ClassBインスタンス化完了');
}
}
※インスタンス化されたことを確認するため、ログを出力するようにしています。
このClassAをインスタンス化するとき、
普通ならこのようにしますね。
$classB = new ClassB();
$classA = new ClassA($classB);
// laravel.log
// local.INFO: ClassBインスタンス化完了
// local.INFO: ClassAインスタンス化完了
まずClassBをnewでインスタンス化して、
そのClassBインスタンスを引数に渡してClassAをnewでインスタンス化します。
この手順を踏まずに
引数を無視していきなり
ClassAをnewでインスタンス化しようとすると、
もちろんエラーが発生します。
$classA = new ClassA();
// laravel.log
// local.ERROR: Too few arguments to function ClassA::__construct()
では次に、サービスコンテナを使ってみましょう。
サービスコンテナを使えば、
事前にClassBをインスタンス化する必要はありません。
いきなりClassAをインスタンス化しちゃいましょう。
$classA = app()->make(ClassA::class);
// laravel.log
// local.INFO: ClassBインスタンス化完了
// local.INFO: ClassAインスタンス化完了
すると、エラーも出ないし
ちゃんとClassAの中でClassBがインスタンス化されています。
これは、サービスコンテナが
「ClassAをインスタンス化するぞ」
「と思ったら、コンストラクタでClassBが型宣言されてるな」
「じゃー先にClassBをインスタンス化しよう」
「ClassBのインスタンス化完了!」
「このClassBインスタンスを引数に渡して、ClassAをインスタンス化しよう」
「ClassAインスタンス化完了!」
ってことをしてくれているわけです。
便利ですね。
さらに、
この依存解決は再帰的に行ってくれます。
つまり、
このClassBがさらにClassCをコンストラクタ引数に持っていたとしても、
さらにClassD、ClassEと入れ子になっていたとしても
全部自動でインスタンス化してくれるというわけです。
class ClassA
{
public function __construct(ClassB $classB)
{
\Log::info('ClassAインスタンス化完了');
}
}
class ClassB
{
public function __construct(ClassC $classC)
{
\Log::info('ClassBインスタンス化完了');
}
}
class ClassC
{
public function __construct()
{
\Log::info('ClassCインスタンス化完了');
}
}
$classA = app()->make(ClassA::class);
// laravel.log
// local.INFO: ClassCインスタンス化完了
// local.INFO: ClassBインスタンス化完了
// local.INFO: ClassAインスタンス化完了
これがサービスコンテナの2つの武器のうちの1つめ
「依存解決」です。
make()
メソッドの中で具体的にどうやって依存解決がされているのか?
というさらに深い部分に関しては
別の記事で解説しています。
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
面白いので興味があれば見てみてください。
【2】インスタンス化方法のカスタマイズ
次に、2つ目の武器
「インスタンス化方法のカスタマイズ」
について解説します。
インスタンス化方法のカスタマイズを定義するには、
サービスコンテナのbind()
メソッドを使います。
app()->bind('呼び出しキーワード', 'インスタンス化方法');
この様に第1引数に、インスタンス化する際の「呼び出しキーワード」を入れ、
第2引数に、インスタンス化方法を記述します。
まずはとりあえず具体例を見てみましょう。
class ClassX
{
public $foo;
public function __construct()
{
\Log::info('ClassX instantiated');
}
}
$foo
プロパティを持っているClassXです。
このClassXのインスタンス化方法をカスタマイズします。
app()->bind(ClassX::class, function () {
$classX = new ClassX();
$classX->foo = 'bar';
return $classX;
});
「呼び出しキーワード」がClassX::class
1で
$classX = new ClassX();
$classX->foo = 'bar';
return $classX;
となっています。
ClassXをインスタンス化するとき、
プロパティ$foo
に文字列bar
をセットする。
というカスタマイズですね。
この状態でインスタンス化するとこうなります。
$classX = app()->make(ClassX::class);
\Log::info($classX->foo);
// laravel.log
// local.INFO: ClassXインスタンス化完了
// local.INFO: bar
ClassXがインスタンス化され、$foo
に文字列bar
がちゃんと入っていますね。
この例のカスタマイズ方法は、
プロパティに文字列を入れる。というだけでしたが、
他にもなんでもできます。
例えば、キーワード「ClassX::class」で呼ばれたら、
ClassYのインスタンスを返す。
なんていうこともできます。
// カスタマイズ方法定義
app()->bind(ClassX::class, function () {
return new ClassY();
});
// 「ClassX::class」キーワードでインスタンス化
$classX = app()->make(ClassX::class);
// laravel.log
// local.INFO: ClassYインスタンス化完了
イメージとしては、
サービスコンテナが
「キーワード」と「インスタンス化方法」の対応表を持っていて、
make()
で呼び出されるたびに
その対応表を確認してインスタンス化してくれているという感じです。
そして、app()->bind()
メソッドを使うことで、
その対応表に追記していっているイメージです。
ちなみに、
このbind()
を使って対応表に追記するこの行為を
「結合」といいます。
上記の例はすごく簡単でシンプルなものでしたが、
結合の方式はもっといろいろあります。
例えばこんなの。
app()->when(Japan::class)
->needs(Car::class)
->give(function () {
return new Toyota();
});
app()->when(America::class)
->needs(Car::class)
->give(function () {
return new Benz();
});
これは、呼び出し元のクラス(make()
を使った場所)
によって、インスタンス化するクラスを変えるというカスタマイズ方法。
app()->when(Japan::class)
Japanクラスの中で
->needs(Car::class)
Car::classのキーワードでmake()したら
->give(function () {return new Toyota();});
Toyotaのインスタンスを返す。
app()->when(America::class)
Americaクラスの中で
->needs(Car::class)
Car::classのキーワードでmake()したら
->give(function () {return new Benz();});
Benzのインスタンスを返す。
という設定になっています。
あとはこんなのとか。
app()->bind(CarInterface::class, function () {
return new CarClass();
});
これは、CarInterface
というインターフェース名で呼び出したら、
返却するインスタンスはCarClass
になるように設定しています。
ちなみに、この
インターフェース名と具象クラスの対応を定義する場合は下記のような
省略記述が可能です。
app()->bind(CarInterface::class, CarClass::class);
あとはすごく重要な機能として、シングルトンによる結合を定義することもできます。
app()->singleton(ClassX::class, function () {
return new ClassX();
});
シングルトンとは何か?については解説しませんので、
他の方の記事などご参考にしてみてください。
サービスコンテナの2つ目の武器
「インスタンス化方法のカスタマイズ」
ですが、こんな風にいろいろと柔軟に使うことができます。
ちなみに、
この結合の定義はどこに書いてもいいのですが、
一般的にここに書くといいよ。と用意されているのが
「サービスプロバイダ」というやつです。
app/Providers/AppServiceProvider.php
は最初から用意されていて、
そのregister()
メソッドに
これまで見たような結合の定義を書いておきます。
このサービスプロバイダは
Laravel起動時に最初に実行される仕組みになっているので、
ここに結合定義を書いておけば
まず最初に「対応表」にインスタンス化のカスタマイズ方法を
全部追記しておくことができるというわけです。
おわりに
サービスコンテナは
「クラスをインスタンス化してくれるマシーン」
であるということ。
そして強力な2つの武器
「依存解決」
「インスタンス化方法のカスタマイズ」
を持っているということ。
を解説していきました。
Laravelフレームワーク自体がこのサービスコンテナの機能を使いまくって動いていて、
実は、自分たちが知らないうちにこのサービスコンテナの恩恵をたくさん受けていたりします。
今回得た知識をもとにLaravelのソースコードを改めて読んでみると
色々発見があって面白いかもです。
Laravelのちょっと深いところを理解していきたいと思っている方、
これらの記事もおすすめですので是非読んでみてください。
(次に進む前に、LGTMしてもらえるとうれしいです)
魔法のようなLaravelも素のPHPに始まり素のPHPに終わる。Laravelのライフサイクルを簡単に解説。
【Laravel】サービスコンテナとは?2つの強力な武器を持ったインスタンス化マシーン。簡単に解説。
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
【Laravel】ファサードとは?何が便利か?どういう仕組みか?
参考
【Laravel】引数でタイプヒントしただけでインスタンスがもらえるのはなぜ?Laravelの魔法を解明してみる。
https://readouble.com/laravel/6.x/ja/container.html
-
「ClassX::class」と書くと、名前空間を含むクラスの完全修飾名を文字列で取得しています
「インスタンス化方法」が ↩