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

サービスコンテナとは

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

Laravelのサービスは?と聞かれると悩んでしまうかもしれませんが、メールを送信する、文字列を暗号化する、ファイルを操作するといったLaravlを使うことで実行できるものがサービスです。

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

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

サービスとコンテナというものがなんとなくわかっていただけたかと思います。ではここで説明したサービスとコンテナのイメージを念頭においてサービスコンテナを理解していきましょう。

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

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

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

サービスというと何か複雑なものを想像してしまいますが、動作確認を行うサービスはただ名前を戻すだけのシンプルな関数です。

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で登録されている

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

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

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


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

dd($name);

ブラウザには、John Doeの文字が表示されます。サービスコンテナに入れる時につけた名前を利用して、入れたものを取り出すことができることもわかりました。

サービスコンテナに入れたものを取り出す方法がいくつもあるので参考に載せておきます。


$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を実行するといつも同じインスタンスを使用します。実際にインスタンスを作成して、違いを確認していきます。

まず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()->bind('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クラスが入れられているので、コンストラクターインジェクションと呼ばれます。[/commebt]

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クラスで置き換えることができればエラーはなくなります。

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

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が表示されます。

まとめ

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