【Laravel】サービスコンテナとサービスプロバイダの仕組みをわかりやすく解説

はじめに

Laravelを使い続けていると、「サービスコンテナって何をしているのか」「サービスプロバイダはどこに何を書けばいいのか」という疑問に当たることがあります。

ドキュメントを読んでも「依存性注入」「バインディング」「解決」といった用語が並んでいて、最初はイメージしにくいかもしれません。

この記事では、サービスコンテナとサービスプロバイダの役割を具体的なコード例とともに整理します。仕組みを理解すると、コードの設計がシンプルになり、テストも書きやすくなります。

サービスコンテナとは

サービスコンテナは、クラスの生成と依存関係の解決を一元管理する仕組みです。「DIコンテナ」とも呼ばれます。

簡単に言うと「クラスを登録しておくと、必要なときに自動で組み立てて返してくれる箱」です。

なぜサービスコンテナが必要か

サービスコンテナを使わない場合、依存するクラスを自分で new して渡す必要があります。

// サービスコンテナなし:依存関係を手動で組み立てる
$mailer   = new SmtpMailer();
$logger   = new FileLogger();
$service  = new UserService($mailer, $logger);

依存するクラスが増えると組み立てが複雑になり、テスト時にモックに差し替えることも難しくなります。

サービスコンテナを使うと、依存関係の解決をコンテナに任せられます。

// サービスコンテナあり:コンテナが依存関係を自動解決
$service = app(UserService::class);

コンテナが UserService のコンストラクタを見て、必要なクラスを自動的に解決してインスタンスを生成します。

自動解決(オートワイヤリング)

Laravelのサービスコンテナは、タイプヒントを見て依存関係を自動解決します。明示的なバインディングが不要なケースも多いです。

<?php

namespace App\\Services;

class MailService
{
    public function send(string $to, string $body): void
    {
        // メール送信処理
    }
}
<?php

namespace App\\Services;

class UserService
{
    public function __construct(
        private MailService $mailService,
    ) {}

    public function register(string $email): void
    {
        // ユーザー登録処理
        $this->mailService->send($email, 'ようこそ!');
    }
}
// コンテナが MailService を自動解決して UserService を生成
$userService = app(UserService::class);

コントローラのコンストラクタでも同様に自動解決が働きます。

<?php

namespace App\\Http\\Controllers;

use App\\Services\\UserService;

class UserController extends Controller
{
    public function __construct(
        private UserService $userService,
    ) {}

    public function store(): void
    {
        $this->userService->register('alice@example.com');
    }
}

バインディング

インターフェースと実装クラスをコンテナに登録することを「バインディング」と言います。これにより、依存先をインターフェースに向けたまま、実装クラスを差し替えられるようになります。

bind

リクエストごとに新しいインスタンスを生成します。

use App\\Contracts\\MailerInterface;
use App\\Services\\SmtpMailer;

app()->bind(MailerInterface::class, SmtpMailer::class);

// 毎回新しいインスタンスが返る
$mailer1 = app(MailerInterface::class);
$mailer2 = app(MailerInterface::class);
// $mailer1 !== $mailer2

singleton

アプリケーションのライフサイクル中に一度だけインスタンスを生成し、以降は同じインスタンスを返します。DBコネクションやキャッシュドライバなど、状態を共有したいクラスに使います。

app()->singleton(MailerInterface::class, SmtpMailer::class);

// 常に同じインスタンスが返る
$mailer1 = app(MailerInterface::class);
$mailer2 = app(MailerInterface::class);
// $mailer1 === $mailer2

instance

すでに生成済みのインスタンスを登録します。

$mailer = new SmtpMailer();
app()->instance(MailerInterface::class, $mailer);

クロージャでバインディング

生成時に追加の設定が必要な場合はクロージャを使います。

app()->bind(MailerInterface::class, function ($app) {
    return new SmtpMailer(
        host: config('mail.host'),
        port: config('mail.port'),
    );
});

コンテナからの取り出し(解決)

バインディングしたクラスを取り出すには app() ヘルパや App::make() を使います。

// 書き方は複数ある(いずれも同じ)
$mailer = app(MailerInterface::class);
$mailer = app()->make(MailerInterface::class);
$mailer = \\App::make(MailerInterface::class);

実務では直接 app() で解決することは少なく、コンストラクタインジェクションで受け取るのが基本です。

サービスプロバイダとは

サービスプロバイダは、サービスコンテナへのバインディングをまとめて定義する場所です。

Laravelのすべての機能(ルーティング・DB・キューなど)はサービスプロバイダを通じて初期化されています。自分のアプリケーションでも、カスタムのバインディングやイベント登録などをサービスプロバイダにまとめて書きます。

config/app.phpproviders 配列に登録されたプロバイダが、アプリケーション起動時に順番に実行されます。

サービスプロバイダの構造

<?php

namespace App\\Providers;

use Illuminate\\Support\\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // バインディングの登録(早期に実行、他のサービスに依存しない処理)
    public function register(): void
    {
        $this->app->singleton(MailerInterface::class, SmtpMailer::class);
    }

    // イベント登録・ルート登録など(すべてのプロバイダのregisterが終わった後に実行)
    public function boot(): void
    {
        // ビューコンポーザ・イベントリスナーの登録など
    }
}

register()boot() の使い分けが重要です。

メソッド実行タイミング用途
register()すべてのプロバイダより先バインディングの登録のみ
boot()すべての register() が完了後他のサービスに依存する処理

register() 内で他のサービスを使おうとすると、まだそのサービスが登録されていない場合があるため注意が必要です。

カスタムサービスプロバイダの作成

php artisan make:provider PaymentServiceProvider

app/Providers/PaymentServiceProvider.php が生成されます。

<?php

namespace App\\Providers;

use App\\Contracts\\PaymentGatewayInterface;
use App\\Services\\StripePaymentGateway;
use Illuminate\\Support\\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(PaymentGatewayInterface::class, function ($app) {
            return new StripePaymentGateway(
                apiKey: config('services.stripe.secret'),
            );
        });
    }

    public function boot(): void
    {
        //
    }
}

Laravel 11以降では bootstrap/providers.php に追加します。

// bootstrap/providers.php
return [
    App\\Providers\\AppServiceProvider::class,
    App\\Providers\\PaymentServiceProvider::class,
];

Laravel 10以前では config/app.phpproviders 配列に追加します。

// config/app.php
'providers' => [
    // ...
    App\\Providers\\PaymentServiceProvider::class,
],

bindings と singletons プロパティ

シンプルなバインディングであれば、register() を書かずにプロパティで定義できます。

class AppServiceProvider extends ServiceProvider
{
    // bind と同等
    public array $bindings = [
        MailerInterface::class => SmtpMailer::class,
    ];

    // singleton と同等
    public array $singletons = [
        PaymentGatewayInterface::class => StripePaymentGateway::class,
    ];
}

実務でよく使うパターン

インターフェースと実装を差し替える

外部サービスへの依存をインターフェースで抽象化しておくと、実装を差し替えやすくなります。

// インターフェース
interface NotificationInterface
{
    public function send(string $message): void;
}

// Slack実装
class SlackNotification implements NotificationInterface
{
    public function send(string $message): void
    {
        // Slack WebhookへPOST
    }
}

// メール実装
class MailNotification implements NotificationInterface
{
    public function send(string $message): void
    {
        // メール送信
    }
}
// サービスプロバイダでバインディング
$this->app->singleton(
    NotificationInterface::class,
    SlackNotification::class
);

テスト時はモックに差し替えるだけで、通知が実際に送信されることなくテストできます。

// テスト
$this->app->instance(
    NotificationInterface::class,
    Mockery::mock(NotificationInterface::class)
);

preventLazyLoadingをboot()で設定する

N+1問題の自動検知のような「全体に影響する設定」は AppServiceProviderboot() にまとめます。

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

macroの登録

boot() でコレクションやリクエストにカスタムメソッドを追加できます。

public function boot(): void
{
    Collection::macro('toAssoc', function () {
        return $this->reduce(function ($assoc, $item) {
            $assoc[$item['key']] = $item['value'];
            return $assoc;
        }, []);
    });
}

まとめ

用語役割
サービスコンテナクラスの生成と依存関係解決を一元管理する仕組み
バインディングインターフェースと実装クラスをコンテナに登録すること
bind()リクエストごとに新しいインスタンスを生成
singleton()一度だけ生成し、以降は同じインスタンスを返す
サービスプロバイダバインディングやブート処理をまとめる場所
register()バインディングのみ記述(他のサービスに依存しない)
boot()register完了後に実行(イベント・マクロ・設定など)

サービスコンテナとサービスプロバイダを理解すると、Laravelのコードが「なぜこう動くのか」がわかるようになり、設計の選択肢も広がります。インターフェースと実装を分離してサービスプロバイダでバインディングする設計を意識すると、テストしやすく変更に強いコードが書けます。

LaravelLaravel

Posted by 千原 耕司