Edited at

Laravelのサービスコンテナとサービスプロバイダはどういうものなのか


なぜこの記事を書いたのか

今社内で Laravel 本の読書会をしたり、公式マニュアルの日本語翻訳ドキュメントを読んだりしながら、Laravelの学習を進めているわけだが、どうもサービスコンテナとサービスプロバイダが重要であることは伝わってくるんだが、その説明がしっくりこない。

なぜなんだろうということで、ソースを読んでみたり、色んな人のまとめを見てみたり、チュートリアルの動画を見てみたりしていて、「あー、こういうものかー」ということで自分的に納得がいったので、それを書いてみたというのがこれになります。

この記事の内容は、あくまでもサービスコンテナとサービスプロバイダの役割のほんの表面をなぞっただけで、全然深堀りしてるものではないです。サービスコンテナとサービスプロバイダは本当にいろいろなことをしていて、これで書いているのはごくごく入り口の部分の理解ということになります。


サービスコンテナとはなにをするものなのか

ドキュメントを読むと、bind メソッド等の 結合 の説明をしたあとに、 make メソッド等の 解決 の説明がしてある。確かにリファレンスとしてはそのとおりなんだろうけど、これを見たときになるほどこう使うのねとは思ったんだが、で、これいつどういうふうに使うの?、そもそもなんのためにあるの? という感想を持ってしまった。

いろいろ調べた結果、自分の理解としては、まずは 解決 側の最小というか基本ルールから積み上げていったほうがしっくりきたので、その順に説明していく。


前提

まず前提として、以下のようなクラスがあるとする。


Foo.php

class Foo

{
public function say()
{
echo "Hello!\n";
}
}


最小のパターン

サービスコンテナの役割の最小のパターンは次のものだと思っている。


  • 引数に渡した文字列に一致するクラスがあれば、それを生成して返却する

app()->make('Foo')->say(); // Hello!

上記の例だと、文字列 "Foo" に対応する Foo クラスが生成できたら返してくださいということになる。前提として、 Foo クラスはあることになっているのでそのインスタンスが返却され、その say メソッドを実行するので、 Hello! と出力される。

最小パターンだとコンテナの中にはなにも入っていない。引数に一致するクラスがあれば生成するということをするだけだ。

これを実現する疑似コード的な意味の雑サービスコンテナを書くとこうなる。


MiniServiceContainer.php

class MiniServiceContainer

{
public function make($key)
{
// クラスが存在したらインスタンス生成
if (class_exists($key)) {
return new $key();
}

throw new RuntimeException('そんなものは作れない');
}
}


make の引数というのは、こういうキーでサービスコンテナが生成できる何かはありますか?という問い合わせに使う文字列でありそれ以上の意味はない。

これを次のように書くとあたかも Foo::class に関係する処理をしているように見えるが、 Foo::class は結局の所 "Foo" という単なる文字列でしかないので、やっぱりそれ以上の意味はない。

app()->make(Foo::class)->say(); // Hello!


別のキーで問い合わせる

次のパターンとして、解決時に素直に存在するクラス名そのもので問い合わせるのではなくて、別の文字列で問い合わせたいというものになる。例えば、 "Bar" という文字列で問い合わせたら、 Foo クラスを返却してほしいという場合である。

この段階で bind メソッドが登場する。第2引数はあくまでも文字列だ。

app()->bind('Bar', 'Foo');

app()->make('Bar')->say(); // Hello!

このパターンのために雑サービスコンテナを拡張するとこんな感じになる。

class MiniServiceContainer

{
protected $container;

public function bind($key, $anotherKey)
{
$this->container[$key] = $anotherKey;
}

public function make($key)
{
// コンテナにキーが登録されていたらキーを差し替える
if (isset($this->container[$key])) {
$key = $this->container[$key];
}

// クラスが存在したらインスタンス生成
if (class_exists($key)) {
return new $key();
}

throw new RuntimeException('そんなものは作れない');
}
}


登録済みのインスタンスを使う

次のパターンとして、ようやく一般的なDIContainerでもありそうなパターンになる。これは、あらかじめ生成しておいたインスタンスをコンテナに登録しておき、それをそのまま返却するというものである。

Laravelではこれは bind メソッドではなく instance メソッドを使う。

$foo = new Foo();

app()->instance('Bar', $foo);
app()->make('Bar')->say(); // Hello!

このパターンを雑コンテナに更に反映してみる。

class MiniServiceContainer

{
protected $container;

public function bind($key, $anotherKey)
{
$this->container[$key] = $anotherKey;
}

public function instance($key, $instance)
{
$this->container[$key] = $instance;
}

public function make($key)
{
// コンテナにキーが登録されていたら
if (isset($this->container[$key])) {
// 登録されているのがオブジェクトならば
if (is_object($this->container[$key])) {
return $this->container[$key]
}
// キーを差し替える
$key = $this->container[$key];
}

// クラスが存在したらインスタンス生成
if (class_exists($key)) {
return new $key();
}

throw new RuntimeException('そんなものは作れない');
}
}


クロージャーを解決する

今回の説明の最後のパターンだが、クロージャーを結合しておいて、それを問い合わせたら、クロージャーそのものではなくクロージャーを実行した結果を返してくれるというものになる。

app()->bind('Bar', function($app) {

return new Foo();
});
app()->make('Bar')->say(); // Hello!

このパターンを反映したら今回の雑コンテナは完成である。

class MiniServiceContainer

{
protected $container;

public function bind($key, $anotherKey)
{
$this->container[$key] = $anotherKey;
}

public function instance($key, $instance)
{
$this->container[$key] = $instance;
}

public function make($key)
{
// コンテナにキーが登録されていたら
if (isset($this->container[$key])) {
// 登録されているのがオブジェクトならば
if (is_object($this->container[$key])) {
return $this->container[$key]
}
// 登録されているのが呼び出し可能だったら
if (is_callable($this->container[$key])) {
return $this->container[$key]($this);
}
// キーを差し替える
$key = $this->container[$key];
}

// クラスが存在したらインスタンス生成
if (class_exists($key)) {
return new $key();
}

throw new RuntimeException('そんなものは作れない');
}
}

で、このパターンが公式ドキュメントや書籍等で最初に説明されるものである。

引数は単なる問い合わせで使う文字列で、それで登録されているものがあればそれをそのまま返却したり、実行結果を返したり、その名前のクラスがあったら返却するよというものであるということをすっ飛ばして、いきなり以下のような説明があったら、「ん? これなにやってるんだ?」とならないだろうか。

app()->bind(Foo::class, function($app) {

return new Foo();
});
app()->make(Foo::class)->say(); // Hello!

このあたり、積み上げて説明してくれていればすっと腑に落ちたのになぁ。

そして、ここまでみてくれば、あの摩訶不思議な singleton というメソッドは、一度返却した値を覚えておいて、次に呼ばれても同じものを返しているだけだねということで納得がいったという感じである。


で、サービスプロバイダはなんのためにあるのか

サービスコンテナがどういうものかの入り口が見えたので、サービスプロバイダが何であるかの入り口もすんなり理解ができるようになる。

サービスプロバイダも、サービスコンテナがもつ コンテナに登録されていないキーで問い合わせたら、そのキーと一致するクラスがあったら生成する という原則があり、それを補助するものということになる。

サービスコンテナの最初の説明のところに以下のような例が載っている。

サービスプロバイダがやりたいことは、この場合だと、 UserContainer クラスを生成する際に、そのコンストラクターに UserRepository クラスを渡すにはどうしたらいいか?ということを解決することである。

<?php

namespace App\Http\Controllers;

use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
/**
* The user repository implementation.
*
* @var UserRepository
*/

protected $users;

/**
* Create a new controller instance.
*
* @param UserRepository $users
* @return void
*/

public function __construct(UserRepository $users)
{
$this->users = $users;
}

/**
* Show the profile for the given user.
*
* @param int $id
* @return Response
*/

public function show($id)
{
$user = $this->users->find($id);

return view('user.profile', ['user' => $user]);
}
}

Laravelのルーティングは、雑に書くとコントローラーのインスタンスを以下のように生成する。(最初ルーティングの設定を見たときに "UserController@index" みたいにクラス名をフルで書くんだなぁと思っていたがこのことを考えるとまぁそのほうが楽だよねと変に納得してしまった)

$controller = app()->make('UserController');

すでにサービスコンテナのところで説明したように、引数で渡された文字列がサービスコンテナに登録されていなかったとしても、そのクラスが存在していればそれを生成して返してくれる。

この UserController のコンストラクタの引数を持ってなければ素直にインスタンス生成されて返却されて終わりなわけだが、今回の場合は UserRegistory が必要となる。

この紐付けにサービスプロバイダである AppServiceProvider#register で以下のように登録しなければならないということになる。

class AppServiceProvider extends ServiceProvider

{
public function register()
{
$this->app->bind(UserController::class, function($app) {
return new \App\Http\Controllers\UserController($app->make(UserRegistry::class));
});
}
}

これでまた、 UserRegistry クラスのコンストラクターに別のクラスのインスタンスを渡さないと行けなくなるならば、更にサービスプロバイダに登録していく...という連鎖が発生するだけである。


まとめ

以上が自分の中でまとめたサービスコンテナとサービスプロバイダの考え方の入り口である。なんのためにあってどういうふうに積み上がったものなのか?ということがわかれば後はその上にどんどん積み上げていけばいいので途端に理解が進んだ感じがする。

もちろん、本来のサービスコンテナは今回の説明で作ったような雑なものではなく、もっとたくさんの機能があるし、コントローラーの生成ももっと複雑なことをしている。が、とっかりをつかんでしまえば、ディープダイブしていくのはかなり楽になる。

この記事でサービスコンテナやサービスプロバイダを理解したぞーという人が出てくれると嬉しいし、これを手がかりにディープダイブしていく人の手助けになるといいなぁと思ってます。

で、自分としてはもっともっと深堀りしていったものをPHPCon東京で話すぞーみたいな感じで今日プロポーザルを書いたのでそれが通るといいなーなんて思ってます。(これが言いたいだけだったりする)