Laravelのサービスコンテナを理解して設計を強くする:依存解決・バインド・スコープ運用まで
- 作成日 2026.03.02
- その他
Laravelのサービスコンテナは「クラス同士の依存関係を組み立てる仕組み」。ControllerやJob、Commandなどが必要とする依存(サービス/リポジトリ/クライアント)を、コンテナが自動で解決して注入する。これにより new の乱立や密結合を減らし、テスト容易性・置換可能性・環境差分(本番/開発)を吸収しやすい構造になる。重要なのは、何でもコンテナに突っ込むことではなく、責務の境界とライフサイクル(singleton/scoped/毎回生成)を決めて運用すること。
- 1. サービスコンテナの役割:依存性注入(DI)をアプリ全体に通す
- 2. まず押さえる用語:IoC / DI / Binding / Resolution
- 3. 自動解決(Auto-Wiring):型ヒントだけで依存が注入される仕組み
- 4. バインドの基本:bind / singleton / scoped を使い分ける
- 5. インターフェースを軸にする:実装差し替えとテストのための基本形
- 6. クロージャバインド:生成時に引数や設定を注入する
- 7. コンテナに入れるもの・入れないもの:責務の境界を崩さない
- 8. Service Providerの位置づけ:registerとbootの使い分け
- 9. コンテナから取り出す方法:resolve / app() / make()
- 10. コンテキスト別の実装差し替え:環境(local/staging/prod)で切り替える
- 11. デコレータ/ラッパで強化する:ログ・リトライ・キャッシュを後付けする
- 12. テストでの置換:mock・fakeをコンテナにバインドする
- 13. アンチパターン:サービスロケータ化と密結合の加速
- 14. 実務の型:Controller→UseCase→Service→Client をコンテナで繋ぐ
- 15. まとめ:サービスコンテナを“便利機能”で終わらせないための要点
サービスコンテナの役割:依存性注入(DI)をアプリ全体に通す
・依存解決:必要なクラスを再帰的に組み立てる
・DI:コンストラクタやメソッド引数に依存を渡す
・置換:実装を差し替える(インターフェース→具象)
・ライフサイクル管理:同一インスタンスを使い回す/毎回作る
「どのクラスが、何に依存しているか」を見える化し、設計の自由度を上げるのが主目的。
まず押さえる用語:IoC / DI / Binding / Resolution
・IoC(Inversion of Control):生成や依存解決をフレームワーク側に委ねる
・DI(Dependency Injection):依存を外から注入する
・Binding:コンテナに「抽象→実装」「キー→生成方法」を登録する
・Resolution:コンテナが登録情報や型情報を使ってインスタンス化する
この4つが分かると、ServiceProviderの意味が一気にクリアになる。
自動解決(Auto-Wiring):型ヒントだけで依存が注入される仕組み
Laravelは、コンストラクタ引数にクラス型が書かれていれば、基本的に自動で解決できる(依存がさらに依存していても再帰的に解決)。
namespace App\Http\Controllers;
use App\Services\BillingService;
class BillingController
{
public function __construct(private BillingService $billing)
{
}
public function store()
{
$this->billing->charge();
}
}発生しやすい問題:
・コンストラクタにスカラー値(string/int)を置いている→自動解決できない
・インターフェース型を要求しているが、バインドが無い→解決できない
バインドの基本:bind / singleton / scoped を使い分ける
・bind:解決のたびに新しいインスタンス
・singleton:アプリ(プロセス)内で同一インスタンスを使い回す
・scoped:リクエスト単位で同一、次リクエストは別(HTTPリクエスト境界で便利)
どれを使うかは「状態を持つか」「スレッド/プロセス境界」「接続の使い回し」次第。
use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// 毎回生成
$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);
// 使い回し(状態を持たせない前提)
$this->app->singleton('feature.flags', function () {
return new \App\Services\FeatureFlags();
});
// リクエスト単位で使い回し
$this->app->scoped(\App\Services\RequestContext::class, function () {
return new \App\Services\RequestContext();
});
}
}
// ServiceProviderで実装を決める
$this->app->bind(\App\Contracts\MailClient::class, \App\Infra\SmtpMailClient::class);インターフェースを軸にする:実装差し替えとテストのための基本形
依存先を具象クラスではなく契約(interface)に寄せると、実装の差し替えが簡単になる。
use App\Contracts\SmsClient;
use App\Infra\TwilioSmsClient;
$this->app->singleton(SmsClient::class, function ($app) {
$config = $app['config']->get('services.twilio');
return new TwilioSmsClient(
accountSid: $config['sid'],
authToken: $config['token'],
from: $config['from'],
);
});クロージャバインド:生成時に引数や設定を注入する
環境変数や設定値を読み込んでクライアントを作る場合、クロージャで生成式を明示すると安定する。
発生しやすい問題:
・config:cache の影響で設定変更が反映されない
・サービス生成時に例外が起き、アプリ全体が起動不能になる(register内で外部通信しない)
コンテナに入れるもの・入れないもの:責務の境界を崩さない
入れると効果が大きい:
・外部APIクライアント(HTTPクライアントラッパ)
・ドメインサービス(決済、請求、通知、権限)
・リポジトリ(DBアクセスの抽象化)
・設定済みのユーティリティ(署名、暗号、ID生成)
入れない方が良いことが多い:
・状態を大量に持つDTOやEntityをコンテナから取る
・その場限りの計算結果
・リクエストデータそのもの(RequestContext等に切り出すなら別)
Service Providerの位置づけ:registerとbootの使い分け
・register:バインド定義、依存の登録(軽い処理だけ)
・boot:他サービスが登録された後に行う設定(イベント登録、ルート、ポリシー、ビュー共有)
registerで外部通信・重い処理をすると、全リクエストが遅くなる原因になる。
コンテナから取り出す方法:resolve / app() / make()
基本はDI(コンストラクタ注入)が最優先。どうしても必要ならコンテナから明示的に取り出す。
use App\Contracts\PaymentGateway;
$gateway = app(PaymentGateway::class);
// または
$gateway = resolve(PaymentGateway::class);
// または
$gateway = app()->make(PaymentGateway::class);注意点:
・どこでも app() し始めると、依存が見えなくなりテストが壊れる
・取り出しは「境界(Factory、ServiceProvider、Bootstrap)」に寄せる
コンテキスト別の実装差し替え:環境(local/staging/prod)で切り替える
例:本番は外部SMS、開発はログ出力だけにする。環境差分をコンテナで吸収するとコードが散らからない。
use App\Contracts\SmsClient;
use App\Infra\TwilioSmsClient;
use App\Infra\LogSmsClient;
$this->app->bind(SmsClient::class, function ($app) {
if ($app->environment('production')) {
return $app->make(TwilioSmsClient::class);
}
return new LogSmsClient();
});デコレータ/ラッパで強化する:ログ・リトライ・キャッシュを後付けする
既存の実装に、横断的関心事(ログ、メトリクス、キャッシュ)を包むと、呼び出し元のコードを触らず強化できる。
use App\Contracts\SmsClient;
use App\Infra\TwilioSmsClient;
use App\Infra\LogSmsClient;
$this->app->bind(SmsClient::class, function ($app) {
if ($app->environment('production')) {
return $app->make(TwilioSmsClient::class);
}
return new LogSmsClient();
});テストでの置換:mock・fakeをコンテナにバインドする
Laravelのテストは、コンテナ差し替えと相性が良い。外部通信を潰し、期待通り呼ばれたかを検証できる。
use App\Contracts\SmsClient;
use Mockery;
public function test_sms_is_sent()
{
$mock = Mockery::mock(SmsClient::class);
$mock->shouldReceive('send')->once();
$this->app->instance(SmsClient::class, $mock);
// ここで対象処理を実行
}アンチパターン:サービスロケータ化と密結合の加速
避けたい形:
・あらゆる場所で app() / resolve() を呼びまくる
・Controllerが巨大になり、何でもコンテナから取る
・ServiceProviderで外部APIへ接続して初期化する
改善指針:
・依存はコンストラクタで宣言する
・境界で組み立て、内部は純粋なオブジェクトに寄せる
・重い初期化は遅延評価にする(必要時に作る)
実務の型:Controller→UseCase→Service→Client をコンテナで繋ぐ
役割分担の例:
・Controller:入出力とHTTP責務だけ
・UseCase:アプリの手順(取引の流れ)
・Service:ドメイン寄りの処理
・Client/Repository:外部・DBアクセス
この構造をコンテナに通すと、差し替えとテストが圧倒的に楽になる。
まとめ:サービスコンテナを“便利機能”で終わらせないための要点
・DIで依存を宣言し、自動解決を基本にする
・抽象(interface)を軸にして実装をバインドする
・bind/singleton/scopedをライフサイクルで選ぶ
・ServiceProviderのregister/bootを使い分け、重い処理を置かない
・横断的関心事はデコレータで後付けし、呼び出し元を汚さない
・テストではinstanceで差し替え、外部依存を切る
-
前の記事
Laravel×Dockerで開発環境を最短で安定させる:ローカル統一・再現性・速度を両立する構成 2026.02.27
-
次の記事
記事がありません
コメントを書く