Laravelのサービスコンテナとサービスプロバイダについて
こんにちは!さっしです!
突然ですが、みなさまは「サービスコンテナ」や「サービスプロバイダ」を理解していますか!?
私は普段の業務でのメインフレームワークとしてLaravelを使っていますが、今まで表面的な部分しか触れていなかったなめ、全然理解していなかったことが最近判明しました(笑)
その為、自分と同じような方のために、自身の理解を深めるために、この記事を書きました!
※本記事では初めてタイトルの内容を聞いた方が、短時間での理解と実装が出来ることを目的としています。その為、本記事では数あるメソッドから代表的なもののみ紹介しています。
より広範囲に、多目的にりたい方は、場合は公式ドキュメントを参考ください。
1.サービスコンテナとサービスプロバイダについて
ここではサービスコンテナとサービスプロバイダの概要、違いについて説明させて頂きます。
1.1.サービスコンテナはどこから来ているのか?
詳細にいく前に、サービスコンテナがどこから来ているのかをまず確認します。
簡単ですが、主に以下の順序で処理がなされます。
1.リクエスト
↓
2.エントリポイント(public/index.php)
↓
3.オートローダーの読み込み(require __DIR__.'/../vendor/autoload.php')
↓
4.フレームワークの起動(require_once __DIR__.'/../bootstrap/app.php')
↓
5.HTTPカーネル(app/Http/Kernel.php)
↓
6.ルータ(Illuminate\Routing\Router\routers\web.php)
↓
7.ミドルウェア
↓
8.コントローラ
サービスコンテナはこの「4.フレームワークの起動(require_once __DIR__.'/../bootstrap/app.php')」のタイミングで本体が読み込まれ、インスタンスが返却される。
このapp.phpに「Illuminate\Foundation\Application」が定義されているが、これがサービスコンテナの本体。
1.2.サービスコンテナについて
・クラスのインスタンス化を管理する仕組み。
・主にサービスプロバイダから呼び出し名とインスタンスの生成方法を登録を登録する(これをバインドまたは結合と呼ぶ)
・コントローラ等から指定されたサービス名からインスタンスを生成して返却する(これを解決と呼ぶ)
つまりクラスを預かり、その預かり物にラベルをつけて管理をし、必要な時にそのラベルを基にクラスを取り出してくれる便利な機能。
1.3.サービスプロバイダについて
・サービスコンテナにインスタンスの生成方法を登録(結合)するために、登録しやすくするための仕組み
・Laravelインストール初期状態でconfig/app.phpの「providers」エリアによって様々なサービスプロバイダが用意されている
・「/app/Providers/AppServiceProvider.php」がすでに用意されているので、初めはこのファイルを使用しても良い
・クラス間同士の依存関係の管理も行う
※サービスプロバイダを使わなくても、サービスコンテナへの登録は可能ですが、サービスプロバイダを使った方が管理等の様々な面から望ましく、公式もサービスプロバイダの使用を推奨している
1.4.概念図
2.実装方法について
ここではサービスコンテナへの具体的な実装方法をご紹介します。
前提として、以下のモデルクラスが存在する事とします。
<?php
namespace App;
class Hoge
{
protected $num;
public function __construct($num = 0) {
$this->num = $num;
}
public function getNum() {
return $this->num;
}
}
2.1.bindメソッドを使用した結合
では、まずはサービスコンテナに結合してみましょう。
以下のコードは、初期状態で存在するHomeController.phpにて宣言しています。
app()->bind(\App\Hoge::class, function() {
return new \App\Hoge();
});
第一引数:サービスコンテナに結合する際の「文字列」を指定する。解決す際にこの文字列を使用する
第二引数:サービスコンテナから返却される「クラス」を指定する。指定時はクロージャー等を使用する
上記メソッドを宣言することにより、サービスコンテナに「\app\hoge」という文字列で「Hogeクラスのインスタンスを返却」する内容が登録されました。
2.2.makeメソッドを使用した解決
では、先ほどサービスコンテナに結合したHogeクラスを解決してみましょう。
$hoge = app()->make(\App\Hoge::class);
echo $hoge->getNum(); // 0が出力される
第一引数:サービスコンテナに結合した際の「文字列」を指定する。
取り出し後に、getNumメソッドを使用していますが、現時点では必ず「0」が返ってきます。
2.3.引数を使用した結合と解決
今まではただインスタンスを生成しただけでしたが、今度は引数を指定してインスタンスを生成してみます。
まず、bindメソッドで以下の様に、クロージャー内で引数を渡せるように結合します。
app()->bind(\App\Hoge::class, function($app, $arg) {
return new \App\Hoge($arg[0]);
});
第一引数:サービスコンテナに結合した際の「文字列」を指定する。
第二引数:クロージャー内にて、$appと$argを指定する。$argでは実際に渡したい引数を「配列形式」で指定する
次に、makeメソッドで以下の様に解決します。
今回は数字の「20」の出力を期待します。
$hoge = app()->make(\App\Hoge::class, [20]);
echo $hoge->getNum(); // 20が出力される
第一引数:サービスコンテナに結合した際の「文字列」を指定する。
第二引数:サービスコンテナに渡したい値を「配列形式」で指定する。
期待通り「20」が出力されたかと思います。
2.4.singletonメソッドを使用した結合
基本的な使い方はbindメソッドと違いはありませんが、シングルトンという名前の通り、サービスコンテナ側で返却するインスタンスを必ず同一のものにしてくれます。
まずシングルトンでクラスが生成されていることが分かりやすいように、最初に作成したHogeクラスを以下の様に変更します。
<?php
namespace App\Spice;
class Member
{
protected $num;
public function __construct($num = 0) {
// $this->num = $num;
$this->num = mt_rand(1, 9999);
}
public function getNum() {
return $this->num;
}
}
これでインスタンス生成時の$numプロパティの値は1~9999のいずれかになります。
次にsingletonメソッドを使い、サービスコンテナに結合します。
app()->singleton(\App\Hoge::class, function() {
return new \App\Hoge();
});
最後にmakeメソッドを使い解決を行います。
今回は分かりやすいように、2回解決処理を行ってみます。
$hoge1 = app()->make(\App\Hoge::class);
echo $hoge1->getNum(); // 100が出力される
$hoge2 = app()->make(\App\Hoge::class);
echo $hoge2->getNum(); // 100が出力される
この結果から$hoge1と$hoge2はまったく同一のオブジェクトとして、解決処理されたことが分かります。
2.5.サービスプロバイダーを使用した結合
「\App\Providers\AppServiceProvider.php」に記載した内容は、Laravel起動時に自動的に読み込まれるように設定がされています。
その為、まずはここに以下のように実装してみます。
public function register()
{
app()->singleton(\App\Hoge::class, function() {
return new \App\Hoge();
});
}
public function boot()
{
}
サービスプロバイダを使用する決まりとして、registerメソッドは必ず実装しなければなりませんが、bootメソッドは任意となります。
また実行タイミングは以下となります。
1.全サービスプロバイダのregisterメソッドが実行される
2.全サービスプロバイダのbootメソッドが実行される
3.サービスコンテナとDIについて
ここでは実際にサービスコンテナとサービスプロバイダーを使用して、どのようにクラス同士の依存度を下げるかを、サンプルコードを用いて解説します。
前提として、以下のモデルクラスが存在する事とします。
<?php
namespace App\Drink;
class Beer
{
public function spark() {
echo 'しゅわしゅわ';
}
}
3.1.クラスの依存度が高い
まずは以下のコードを見てください。下記は適当なコントローラー内に記述したアクションになります。
/**
* Beer(クラス)に依存している
*/
public function drink()
{
$beer = new \App\Drink\Beer();
$beer->spark();
}
このアクションでは、アクション内でインスタンスを生成しています。そのためBeerクラスが無いと結果としてこのアクション自体が成り立たなくなっています。つまりクラスに依存している状態と言えます。
3.1.2.クラスの依存度が低い
ではこの問題を踏まえて、各種コードを変更してみましょう。変更するコードは4つです。
1.インターフェースを作成
2.Beerクラスを修正
3.サービスプロバイダに登録
4.アクションを修正
1.インターフェースを作成
まず、Beerクラスを抽象化したインターフェースを作成します。
<?php
namespace App\Drink;
interface SparklingInterface
{
public function spark();
}
2.Beerクラスを修正
次に、事前に作成しているBeerクラスにSparklingInterfaceを実装します。
<?php
namespace App\Drink;
use App\Drink\SparklingInterface;
class Beer implements SparklingInterface
{
public function spark() {
echo 'しゅわしゅわ';
}
}
3.サービスプロバイダに登録
ここではインターフェース名をキーとして、Beerクラスのインスタンスを返却するようにします。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Drink\SparklingInterface;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(SparklingInterface::class, function() {
return new \App\Drink\Beer();
});
}
4.アクションを修正
ここでアクションの引数に、先ほど作成したインターフェース名をタイプヒントした$somedrinkという変数を記述します。これを「コンストラクタインジェクション」といいます。
コンストラクタインジェクションは、クラス名がタイプヒントされた引数を指定すると、そのクラスのインスタンスを自動生成するというものです。
/**
* ビール(クラス)に依存させない
*/
public function drink(\App\Drink\SparklingInterface $somedrink) // ←コンストラクタインジェクション
{
$somedrink->spark();
}
ご覧いただくと分かるように、アクションから完全にBeerクラスへの依存がなくなりました。このことにより、例えば新しくインターフェースを継承した「NonAlcoholBeerクラス」クラスをサービスコンテナから返却されるクラスに切り替えるといった事も、サービスコンテナとサービスプロバイダーの機能を用いればいとも簡単に実装が出来ます。
4.まとめ
いかがでしたでしょうか?
恐らく全てのプロジェクトでこの機能を使うという事はあまり考えられませんが
・singletonを実装したい
・DIのデザインパターンを実装したい
といった内容が頭に浮かんだ際は、この機能を使って実装する方がより短時間でメンテナンスのしやすいプログラムになるのでないかと思います。
今回紹介できませんでしたが、まだほかにもたくさんのメソッドがありますので、ぜひ使ってみてください!