メシのタネ

めしのたねになるIT情報配信サイト


LaravelのサービスコンテナとDI、「書いてるだけで動く」コードの正体


  1. Laravel
  2. LaravelのサービスコンテナとDI、「書いてるだけで動く」コードの正体

Laravelを書いていて、「なんでこれ勝手にインスタンス入ってるの?」って思ったこと、ありませんか?
それ、DI(依存性注入)っていう仕組みのおかげなんですが……正直、仕組みわかってなくてもLaravelって動いちゃうんですよね。

本記事では、「バインドって何?」「インターフェイスって必要なの?」「app()っていつ使うの?」みたいな疑問を、ちょっとゆるめのノリで解きほぐしていきます。

テストが書きやすくなる、保守がしやすくなる、なんとなく設計がスマートに見える(大事)——そんなDIの恩恵を、今よりちょっとだけ実感できるようになる記事を目指しました。

目次

はじめに

DIって、結局「newしないやつ」ってことでしょ?

Laravelを触っていて、「なんかインスタンス勝手に入ってきてるな……」と思ったことありませんか?
それ、DI(依存性注入)という仕組みのおかげです。けど、名前が仰々しいせいで、
「なんか高度な設計の話?」って構えちゃう人、多いと思います(筆者含む)。

でもDIって、ほんとはめちゃくちゃ地味なやつなんです。
ざっくり言うとこういうこと:

🤖 DIじゃない場合(自力でnew)

$userService = new UserService();

🎁 DIしてる場合(Laravelに任せる)

public function __construct(UserService $userService)

Laravelが勝手にUserServiceのインスタンスを用意して渡してくれます。
「自分でインスタンス作らない代わりに、外からもらう」。これがDIの中核です。

なんでDIが大事なの?

Laravelでは、DIを使うことでいろんなメリットがあります:

  • テストがしやすくなる
    → テスト用のモックやスタブを差し替えるのが簡単になる
  • 保守がラクになる
    → 実装クラスを変更しても、呼び出し元のコードをいじらずに済む
  • クラスの責務が明確になる
    → 「このクラスはこれが必要」と明示できるようになる

DIは、「Laravelっぽいコード」の基礎にある考え方。
でも言葉で説明されるとよくわからない部分もあるので、この記事では、実際のコード例やよくある使い方を交えて、
「DIってそういうことか!」と思えるようなガイドを目指していきます。

LaravelにおけるDIについての詳細解説

サービスコンテナの概要

Laravelには、「サービスコンテナ(Service Container)」という、超便利なおせっかい屋さんがいます。
こいつは、必要なクラスを勝手に作って渡してくれるやつです。

たとえば、こんなコード:

class UserController extends Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function index()
    {
        return $this->userService->getAllUsers();
    }
}

このコード、UserControllerUserService自分でnewしてないですよね?
でもLaravelは動く。なぜかって?サービスコンテナが勝手に用意してくれるから。

Laravelのサービスコンテナはこんな仕組み:

  • 「UserServiceが必要なんだな」と型ヒントから察知
  • もしバインドされていれば、それに従ってインスタンス化
  • 何も指定されてなくても、クラスに引数がなければ勝手にnewする
// 自動的に解決される例(引数なし)
class NotificationService
{
    public function send($message)
    {
        // メッセージを送る処理
    }
}

// これを注入
class AlertController extends Controller
{
    public function __construct(NotificationService $service)
    {
        $service->send("Hello!");
    }
}

この場合、NotificationServiceに特別な設定がなくても、Laravelが自動でnewしてくれます。
「あ、引数ないクラスね?任せて!」って感じ。

🧰 インターフェイスを使うときはバインドが必要

Laravelは、具体的なクラス名が書いてあれば勝手にインスタンスを作ってくれます。
でも、「このクラス、どの実装を使えばいいの?」ってのが明示されてないと、迷子になります。

たとえば、インターフェースをDIしようとした場合:

class UserService
{
    public function __construct(UserRepository $repo)
    {
        $this->repo = $repo;
    }
}

ここでUserRepositoryはインターフェースですが、Laravelくんは何を渡せばいいか分かりません。

interface UserRepository
{
    public function getAll();
}

class EloquentUserRepository implements UserRepository
{
    public function getAll()
    {
        return User::all();
    }
}

インターフェースはあくまで「こういう操作ができるよ」というルール。
そのままではインスタンス化できない
ので、Laravelはこう思います:

🐣「UserRepositoryね……え?それどのクラスをnewすればいいの???」
🐤「インターフェースはnewできないんですけど〜〜〜???」

🔧 だから必要なのが「バインド」

// AppServiceProvider.php
public function register()
{
    $this->app->bind(
        UserRepository::class,
        EloquentUserRepository::class
    );
}

これでLaravelのサービスコンテナは、「UserRepositoryって言われたらEloquentUserRepositoryを使えばいいんだな」と理解できます。

以降は、どこでUserRepositoryを型ヒントしても、自動で正しい実装が注入されるようになります。

💡 バインドはどこに書くの?

Laravelプロジェクトには、最初から App\Providers\AppServiceProvider というクラスが用意されています。

app/Providers/AppServiceProvider.php を開いて、register() メソッドの中にバインドのコードを書きましょう。

public function register()
{
    $this->app->bind(
        UserRepository::class,
        EloquentUserRepository::class
    );
}

Laravelでは、このAppServiceProviderサービスの登録場所として用意されている公式な入り口です。

ファイル名に「Service」とか「Provider」ってついてて不安になるかもしれませんが、ここは触ってOKな場所です。
「え、これ自分で書いていいの?」って思ってた人、正解です。書いていいやつです。

バインドの数が増えてきたら、専用のServiceProviderを作ってもOKです。
php artisan make:provider RepositoryServiceProvider とかして分けてもいい。
(でも最初はAppServiceProviderでOK)

bind()とsingleton()の違い:インスタンスっていつ作るのが正解?

ここまでで、Laravelがサービスコンテナを通じてクラスを注入してくれる仕組みがわかってきました。
でも実は、「サービスコンテナにどうやって登録するか」でも挙動が変わります。

たとえば、同じクラスを使っていても:

  • 毎回新しいインスタンスがほしい場合
  • 一度だけ作ったインスタンスを使いまわしたい場合

この2つのどっちかで、Laravel側の動きが変わります。
これを制御するのが、bind()singleton()です。

まず、bindsingletonでAppServiceProviderに登録:

// AppServiceProvider.php
public function register()
{
    $this->app->bind(SomeClass::class, function ($app) {
        return new SomeClass();
    });
    
    $this->app->singleton(AppConfigManager::class, function ($app) {
    return new AppConfigManager(); // 最初の1回だけ
});
}

bindで登録したサービスコンテナを呼び出す:

$one = app(RandomNumberGenerator::class);
$two = app(RandomNumberGenerator::class);

$one === $two; // false

🤖 別のインスタンスとしてインスタンス化される。

singletonで登録したサービスコンテナを呼び出す:

$a = app(AppConfigManager::class);
$b = app(AppConfigManager::class);

$a === $b; // true

🤖 同じインスタンスとしてインスタンス化される。

🙄 ほーん、で?それ意味あるんすか?ってあなたへ

✅ bindのいいところ:

  • 毎回newされる=副作用がない
  • テスト中でもインスタンスが汚れない
  • 「このインスタンス、他で誰か使ってないよね?」って不安がない

🧨 singletonの危ないところ:

  • サービスが状態を持ってたら、全呼び出しで影響を受ける
  • 気づかないうちに共有して、バグになってることがある
  • テストで「なんか前の値残ってる」系の地獄が始まる

💬 結論(読者用まとめ):

設計に自信がないなら、bindしておけば安全。
singletonは便利だけど、「このインスタンス、絶対に変な状態にならない」と言い切れるときだけ使うのが吉。

singletonってなに?

class MemoPad
{
    public array $notes = [];

    public function write(string $note)
    {
        $this->notes[] = $note;
    }
}

// AppServiceProvider.php に↓を登録
$this->app->singleton(MemoPad::class, fn() => new MemoPad());

$pad = app(MemoPad::class);
$pad->write("やること:洗濯");

$another = app(MemoPad::class);
$another->write("やること:Laravel");

print_r($another->notes); 
// ["やること:洗濯", "やること:Laravel"]

こんな感じで、新しくインスタンス化したのに、前の状態が残ってるってこと。記憶力に自身がない人は素直にBind使おうね。おじさんとの約束だぞ⭐️

たねまる

設定情報とか、アプリ全体で共有したい値を持ってるだけのClassとかだったら、singleton使うのがいいのかもね〜

app() / resolve() / make():Laravelで自分でDIするとき

ここまで読んで、「Laravelが勝手に注入してくれるって便利だな〜」と思ってるあなたへ。
それ実際には:

  • app() は Laravelのグローバルヘルパ関数で、
  • resolve() はそのラッパー的存在で、
  • app()->make() はコンテナのメソッドを直接呼んでる

みたいな関係性があるんですが、
「インスタンスを取得する」っていう目的では、基本的にどれでも同じように使えます。

ただし:「引数付きでインスタンス作りたい」ってときは make()が一番素直に書ける、ってことだけ覚えといてくれると◎です。

特徴まとめ

メソッド実態特徴
app()ヘルパ関数書きやすくて一番使われる。resolveのラッパー。
resolve()Laravel独自関数DIの雰囲気がある。app()とほぼ同じ挙動。
app()->make()Applicationのメソッド引数付きインスタンス生成が一番自然に書ける。

app()->make()の利用例: こんなふうに 「コンストラクタに値を渡しつつDIしたい」ってとき、makeは便利。

class SomeClass
{
    public string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

// AppServiceProvider.phpに登録しなくても使える
$someClass = app()->make(SomeClass::class, ['name' => 'たねまる']);

echo $someClass->name; 
// 結果: たねまる

Laravelは助けてくれる。でも、あなたの設計までは助けてくれない

── サービスロケータ vs 依存性注入の微妙なライン
LaravelのDIって、便利すぎる。
引数に型書けばインスタンスが注入されるし、app()resolve()でいつでも取り出せる。
でも、ここでちょっと立ち止まって考えたい。

これ、本当に「DI」なんだっけ?

🤔 app()で取ってるだけじゃん問題

class SomeController extends Controller
{
    public function handle()
    {
        $userService = app(UserService::class);
        $userService->doSomething();
    }
}

↑これ、DI“っぽい”けど、よく見るとただの手動で依存を取りに行ってるだけ

依存性注入じゃなくて、依存性自己調達
これは「サービスロケータ」と呼ばれるパターンです。

🏷 サービスロケータとは何か?

必要なクラスを、自分でサービスコンテナから引っ張ってくる設計。

DIは「クラスに必要なものを外から渡す」設計。
でもサービスロケータは「必要なものを中で取りに行く」設計。

どっちも動くし、Laravelではどっちもできちゃう。
でも、設計上は大きな違いがある。

⚠️ サービスロケータの何がまずいのか?

  • 依存がコードに見えない
  • テストしづらくなる(中で勝手にapp()されたらモックできない)
  • 読み手が「このクラスが何に依存してるのか」を一発で把握できない

結果:コードが「動くけど読みにくい」ものになっていく。

🧠 本来、サービスロケータは「密結合を避けるための設計支援」だった

本来、DDDやクリーンアーキテクチャでは、
サービスロケータは「newしなくてもインターフェースで解決できるようにしよう」っていう善意で登場したやつ。

でも、Laravelではこれが簡単にどこでも使える便利関数になってしまって、
「使いすぎると、結局newと変わらん設計」になっちゃう。

🧨 instanceofが出てきたら、設計の黄信号

$payment = app(PaymentProviderInterface::class);

if ($payment instanceof StripeProvider) {
    $payment->chargeWithExtraLogging($invoice);
} elseif ($payment instanceof PaypalProvider) {
    $payment->chargeWithSandboxMode($invoice);
}

🤯 本来、interfaceがあるってことは…

  • 呼び出し元は「このメソッドさえ使えれば中身は何でもいい」っていう状態になるべき
  • でも、instanceofが出てくるってことは、「中身によって使い方変えてる」ってこと
  • つまり、それもうinterfaceで隠せてない状態💀

✅ make()は便利だけど、「どういう場面で使うか」を意識しよう

👌 OKパターン:

  • ServiceProviderの中で明示的にインスタンス作りたいとき
  • ファクトリ的に「使う実装を動的に切り替える必要があるとき」
  • クロージャや匿名関数の中で、あえて呼び出したいとき

⚠️ NG寄りパターン:

  • 普通のサービスクラスやコントローラーの中で、毎回app()make()を直接書いてる
  • どの依存が必要かがコードを読まないと分からない状態になってる
たねまる

スコープが閉じてれば使ってもいいってことだよね〜。

俺がDIの成功例だと思ってるやつ

ここまで偉そうに語ってきましたが、DIよく知らない時は単にコーディングルールをなぞってるだけだった。
なんかよくわからんけど、エラーになるからServiceProviderに登録して、UseCaseでDIして、まぁいいやろみたいな感じ。

ただ、UseCase内でビジネスロジックをアプリケーションサービスとして抽象化してみたときにいいないいなDIっていいなって思ったんだ。

✨ 成功例(だとおもうやつ):UseCaseに抽象サービスを注入して動かす構成

// サービスの抽象
interface NotificationServiceInterface
{
    public function sendNotification(string $message): void;
}

// サービス本体
class SlackNotificationService implements NotificationServiceInterface
{
    public function sendNotification(string $message): void
    {
        // Slackに通知を送る処理
        echo "Slack: {$message}";
    }
}

// サービスコンテナ登録
// AppServiceProvider.php
public function register()
{
    $this->app->bind(
        NotificationServiceInterface::class,
        SlackNotificationService::class
    );
}

// ユースケース
class NotifyUserUseCase
{
    public function __construct(
        private NotificationServiceInterface $notificationService
    ) {}

    public function execute(string $message): void
    {
        $this->notificationService->sendNotification($message); // ← この一行で、責務の分離・DIの意味・設計の綺麗さが全部わかる。激エモ。
    }
}

// Controller
class UserController extends Controller
{
    public function __construct(private NotifyUserUseCase $useCase) {}

    public function notify()
    {
        $this->useCase->execute('ようこそ、Laravelワールドへ!'); //sampleなのでゆるしてくだちい。
    }
}

💥 ここで感じるDIの快感ポイント:

  • NotifyUserUseCaseどこから何が来てるか一切気にしてないのに使える
  • 呼び出し側も 依存を気にせず引数に書くだけ
  • 実装を変えたくなったら、ServiceProviderのバインドだけ変えればOK

おわりに

自分の技術知見をちょっと棚卸ししてみるか、くらいの気持ちで書き始めたら、
気づいたらとんでもない文字数になってました。DI、恐るべし。

この記事を書いた理由は、実際に自分がSaaSっぽい何かを作らなきゃいけなくなって、
「どうせやるなら設計ちゃんとやりたいな…」って思ってLaravelの記事を読み漁ってたときの記憶があるからです。

その頃、DIについてまともに解説されてる記事が見つけられなくて、
半端に読んだせいでまさにこの記事で紹介したようなサービスロケータがごろごろ転がってました。
テストについてまだこのブログで書いてないんですが、Mock刺せないんで意味のあるテストがしづらかったです。

もし今この記事を読んでくれてる人が、
当時の自分みたいに「DIってなんなん…」「どう使えばいいの…」ってなってるなら、
少しでもその迷いを減らせたら嬉しいです。

抽象化とかの説明を全く入れてないのが心残りですが、まぁそれは別の機会に記事にしようと思います。
質問とかコメントあればお気軽に書き込んでください。それでは良いDIライフを。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.

若い頃、「仕事中にハマったこと」や「誰かに共有したい技術的な気づき」をアウトプットしたくてブログを始めましたが、勢い任せでよく分からない記事を大量生産し、あえなく飽きて終了。

改めて今、キャリア15年分の経験や知識が、これからITエンジニアを目指す方や、同じような課題で悩んでいる現役エンジニアの「メシのタネ」になるような記事を残したいと思っています。
※過去の記事は見ると精神が崩壊するため、そっとしておいてください。

🛠 経歴という名の珍道中:
文系Fラン → 広告営業 → Web営業 → 通信営業 → Web進行 → 出版 → Web媒体運用 → ソフトウェアハウス → SES → フリーランス

専門教育も受けず、転職歴も多数。履歴書はまるで時系列の事故記録のようですが、試行錯誤を重ね、なんとかエンジニアとして食べています。

このブログでは、そんな「履歴書クラッシャー型エンジニア」が送る、
名古屋一敷居の低い、実務に役立つ技術ブログを目指します。

Laravel
LaravelのサービスコンテナとDI、「書いてるだけで動く」コードの正体New!!
Laravel
Laravelのアーキテクチャ、実は誰もわかってない説New!!
ガジェット
【解説】Bluetoothヘッドホンでマイクが使えない理由と回避策まとめ(Mac対応)New!!
Laravel
Laravel Collection入門: mapとeachの違い、ちゃんと説明できますか?New!!
Laravel
Eloquentのリレーション徹底解説New!!
Laravel
Eloquentは知ってる。でも“理解してる”って言えるのか?New!!