Laravel入門者でLaravelのコアであるサービスコンテナ(Service Container)を理解できている人はそれほど多くはありません。サービスコンテナを理解しなくてもLaravelが使えること、Laravelのマニュアルや公開されている文書を読んでも説明が難解であるため理解するのが難しいことが原因だと考えられます。本文書では難しい用語をできるだけ省いて簡単に説明をおこなっているのでサービスコンテナがどのようなものか理解したい人はぜひ参考にしてください。

サービスコンテナはLaravelのコアな部分なのでバージョンが上がっても本文書で説明を行っている処理に大きな違いはないので本文書は新しいバージョンのLaravelでも使える内容です。
fukidashi

サービスコンテナとは

サービスコンテナという言葉は前半のサービスと後半のコンテナという単語に分けると理解するのが簡単です。

サービスという単語が出てきましたが、ではLaravelのサービスとは何でしょう?

Laravelのサービスは”メールを送信する”、”文字列を暗号化する”、”ファイルを操作する”といったものです。

コンテナはいろいろなサービスが入った入れ物だと考えることができます。実際にコンテナは荷物の輸送に利用される入れ物です。コンテナと聞いてすぐに思い浮かぶのはDockerかもしれませんがそれは今はおいておきましょう。

Laravelのサービスコンテナは入れ物なので入れ物にサービスを入れる時に名前をつけることができ、名前を指定することで入れ物からサービスを取り出して使うことができます。また、サービスを入れるだけではなく入れたものを管理する機能も持っています。

管理する機能というのはこの時点では理解できないと思うのでここではコンテナは入れ物だということだけ理解しておいてください。
fukidashi

サービスというものははっきりしたと思いますが、コンテナはまだまだ曖昧だと思いますが先程説明したサービスとコンテナのイメージ(入れ物)を念頭においてサービスコンテナを理解していきましょう。

サービスコンテナの使い方

サービスコンテナに入れる

サービスコンテナの中にはサービスが複数入っているという話をしました。そのサービスコンテナには追加でサービスをいれることができます。サービスコンテナの中に サービスを入れるためにbindメソッドを利用します。

サービスというと何か複雑なもの組み込まないといけない機能だと思うかもしれませんが、これから動作確認を行うために作成するサービスはただ名前を戻すだけのシンプルな関数です。
fukidashi

bindメソッドを使ってサービスコンテナにサービスを入れてみましょう。

サービスコンテナの中身はヘルパー関数のapp()で確認することができます。bindメソッドを実行する前にweb.phpファイルの中でdd(app())を実行してサービスコンテナの中身を確認しておきます。

下記がapp()の結果です。bindを使ってサービスコンテナへの追加を行うと赤四角の線のbindigsの数字が増えます。bindを実行する前は、41であることを確認しておきます。

bindを行う前の内容
bindを行う前の内容

サービスコンテナに入れる時には取り出す時に利用する名前をつけます。ここではmyNameという名前をつけてサービスコンテナに入れます。サービスと言えるほど何かを提供してくれるものではありませんが、入れるサービスはシンプルな関数で、実行するとJohn Doeの文字列が戻ってきます。


app()->bind('myName', function(){
    return 'John Doe';
});

bindメソッドを実行した後にbindingsの数が増えているかapp()を確認します。bindingsの数字が41から42に増えていることが確認できます。

bind実施後のapp()の中身
bind実施後のapp()の中身

#bings:array42の矢印を展開するとbindメソッドを実行した時に指定したmyNameという名前もサービスコンテナの中に登録されていることも確認できます。

myNameで登録されている
myNameで登録されている

bindメソッドを行うことでサービスコンテナに追加できることがわかりました。これでサービスコンテナが入れ物だということがわかってもらえたかと思います。またサービスコンテナにサービスを入れるのもこんなにシンプルだということがわかったのではないでしょうか。

サービスコンテナから取り出す

サービスコンテナに入れたサービスはmakeメソッドを利用して取り出すことができます。


$name = app()->make('myName');

dd($name);

ブラウザには”John Doe”の文字が表示されます。たったこれだけの処理です。

サービスコンテナに入れる時につけた名前を利用して、入れたものを取り出すことができることもわかりました。

上記ではapp()->make(‘myName’)を使ってサービスを取り出しましたが、サービスコンテナに入れたものを取り出す方法がいくつもあるので参考に載せておきます。どれも取り出されるサービスは同じです。


$name = app()->make('myName');
$name = app('myName');
$name = resolve('myName');
$name = App::make('myName');

依存関係のあるクラスをbindする

サービスコンテナは入れ物だけではなく、入れたものを管理する機能を持つという話を最初にしました。ここではサービスコンテナの管理機能の一部である依存関係の自動解決機能について確認を行なってきます。依存関係の自動解決機能??さっきまで簡単な用語ばかりだったのに急に難しい名称が出てきました。内容はそれほど難しくはありませんが、入門者の人であれば理解できたとしてもだからという疑問があるかもしれませんがいずれ役に立つ知識です。

依存関係のある2つのクラスMyClassクラスとSlackクラスを準備します。


namespace Reffect;

class Myclass
{

    public $slack;

    public function __construct(Slack $slack){

        $this->slack = $slack;

    }

    public function run(){

        $this->slack->send();

    }
   
}

Slackクラスはsendメソッドを持っているだけのシンプルなクラスです。


namespace Reffect;

class Slack
{

    public function send(){

        dd('something happens');

    }
   
}

通常であれば下記のようにコードを記述し実行します。MyClassクラスはSlackクラスに依存しているので、先にSlackクラスをインスタンス化してその変数をMyClassクラスに渡す必要があります。


$slack = new \Reffect\Slack();

$myClass = new \Reffect\MyClass($slack);

$myClass->run();

次にbindメソッドを使ってサービスコンテナへの登録を行います。


app()->bind('myclass', \Reffect\MyClass::class);

bindメソッドでサービスコンテナの登録を行い、下記のコードを実行するとサービスコンテナによって自動的に依存関係が解決され正常に実行することができます。


$myClass = app()->make('myclass');

$myClass->run();

MyClassクラスはSlackクラスに依存しているため、自動で解決が行わなければエラーになるはずです。実際に上記のbindメソッドではSlackについて何も処理を行っていません。しかし、サービスコンテナが持つ依存関係の自動解決機能によって実現されています。

自動解決を利用せずにサービスコンテナへの登録は下記のように記述しても問題なく動作します。下記の場合は、インスタンス化した$slackをMyClassの中に入れているのでどのような処理が行われているのか明白です。


app()->bind('myclass', function(){

	$slack = new \Reffect\Slack();

	return new \Reffect\MyClass($slack);

});

今回は、シンプルな依存関係でしたが、複雑な依存関係を持っている場合もサービスコンテナによって依存関係を解決することができます。

singletonメソッドとbindメソッドの違い

サービスコンテナへの登録はbindメソッド以外にも存在します。ここではsingletonメソッドとの違いを確認します。

bindメソッドでは実行する度に新しいインスタンスを作成しますが、singletonを実行するといつも同じインスタンスを使用します。実際にインスタンスを作成して2つの違いを確認していきます。

まずbindメソッドで登録したmyclassを使って2つのインスタンスを作成します。


app()->bind('myclass', \Reffect\MyClass::class);

$myClass = app()->make('myclass');

$myClass2 = app()->make('myclass');

dd($myClass,$myClass2);

ddコマンドを実行すると下記のように別の識別番号が付与されているので別のインスタンスとして作成されていることが確認できます。


Myclass {#147 ▼
  +slack: Slack {#146}
}
Myclass {#149 ▼
  +slack: Slack {#148}
}

次にbindメソッドをsingletonメソッドに変更して実行します。


app()->singleton('myclass', \Reffect\MyClass::class);

$myClass = app()->make('myclass');

$myClass2 = app()->make('myclass');

dd($myClass,$myClass2);

下記のように同じ識別番号が付与されるので、同じインスタンスであることが確認できます。


Myclass {#147 ▼
  +slack: Slack {#146}
}
Myclass {#147 ▼
  +slack: Slack {#146}
}

bindメソッドのsingletonメソッドの違いがわかってもらえたかと思います。

インターフェイスの依存関係を解決

これまでの動作確認で、サービスコンテナを利用すると自動で依存関係を解決してくれることがわかりました。先程は、Slackクラスを使っていましたが、別のクラスとの交換を容易に行えるようにインターフェイスを使って書き換えます。

Messsag.phpを追加作成します。中身はsendメソッドのみをもつインターフェイスです。


namespace Reffect;

interface Message{

    public function send();
    
}

SlackクラスはMessageインターフェイスを継承させます。


namespace Reffect;

class Slack implements Message
{

    public function send(){

        dd('something happens by Slack');

    }
   
}

Slackクラスと交換できるMailクラスを追加作成します。こちらもMessageインターフェイスを継承させます。


namespace Reffect;

class Mail implements Message
{

    public function send(){

        dd('something happens by Mail');

    }
   
}

最後にメインのMyClass.phpで外部から入れているSlackクラスをMessageインターフェイスに変更します。

コンストラクターの部分で外部からMessageクラスが入れられているので、コンストラクターインジェクションと呼ばれます。
fukidashi

namespace Reffect;

class Myclass
{
    
    public $message;

    public function __construct(Message $message){

        $this->message = $message;

    }

    public function run(){

        $this->message->send();

    }
   
}

変更作業は完了です。bindメソッドでMyClassをサービスコンテナに登録します。この部分に関しては、先ほどまでのコードから変更はありません。


app()->bind('myclass', \Reffect\MyClass::class);

$myClass = app()->make('myclass');

$myClass->run();

しかし、実行するとBinding Resolution Exceptionが発生します。MyClassで外部から入れているのがMessageインターフェイスのためにインスタンス化できずエラーになっています。このMessageインターフェイスを実際に行う処理が記述されているSlackクラスまたはMailクラスで置き換えることができればエラーはなくなります。

インターフェイスは継承クラスが持つメソッドを記述した設計図のようなもので、インターフェイスの中には実処理は記述されていません。ここではMessageインターフェイスはsendメソッドを持っていますが、中身はなく、インターフェイスを継承したSlackクラスやMailクラスの中のsendメソッドに実処理を記述しなければなりません。
fukidashi
Binding Resolution Exception
Binding Resolution Exception

サービスコンテナにはインターフェイスを置き換えるための方法が準備されており、以下のbindメソッドを使うことでMessageインターフェイスをSlackクラスにひもづけることができます。


app()->bind(\Reffect\Message::class, \Reffect\Mail::class);

上記を設定して実行するとエラーが解消され正常に動作し、ブラウザには、”something happens by Mail“が表示されます。

MailクラスからSlackクラスに変更したい場合は、下記のように変更します。


app()->bind(\Reffect\Message::class, \Reffect\Slack::class);

変更後、再度実行すると”something happens by Slack“がブラウザに表示されます。

このようにインターフェイスを利用することでそのインターフェイスを継承クラスを簡単に取り替えることができます。

バインドなしでのサービスコンテナの利用

サービスコンテナを利用するのにbindメソッドやsingleメソッドを使ってきました。しかし、バインド処理を行わなくても下記のように処理を実行することができます。


class Test{
	public function run(){
		dd('test');
	}
}


Route::get('/', function () {

    app(Test::class)->run();

    return view('welcome');
});

Testクラスのrunメソッドが実行され、ブラウザにはtestが表示されます。

まとめ

ここまで読み終えた方は読む前よりもサービスコンテナの理解が進んだかと思います。サービスコンテナの次はサービスプロバイダー、ファサードの説明へと進んでいきます。