📝

Laravelでサービスコンテナを利用した依存性管理

2023/12/31に公開

IoCコンテナについて

Laravelのサービスコンテナを理解するためには、まずIoC(Inversion of Control)コンテナについて理解することが重要です。IoCコンテナは、クラスの依存関係を柔軟に管理する役割を果たしています。

「制御の反転」を意味するIoCは、設計原則の一つであり、モジュール間の疎結合を促進することを目的としています。この記事では、「モジュール」を個々のクラスやコンポーネントとして取り扱います。これにより、各クラスは独立して動作可能となり、テストや再利用が容易になります。

IoCとDI

IoCの原則を実現する一つの手法として、DI(Dependency Injection)があります。
DIはクラスが自身の依存オブジェクトを直接生成するのではなく、外部から注入するデザインパターンです。
したがって、IoCコンテナはDIコンテナとも呼ばれます。

依存しているコードの例

以下に示すPostStoreActionは、PostRepositoryPostLimitCheckerに依存しています。
※ コードはサンプルで動作確認はしていません。

<?php

declare(strict_types=1);

namespace Sample\PHP;

use App\Models\Post;
use App\Models\User;
use Sample\PHP\Infrastructure\PostRepository;
use Sample\PHP\Infrastructure\PostLimitChecker;
use Exception;

class PostStoreAction
{
    private $postRepository;
    private $postLimitChecker;

    public function __construct() {
        $this->postRepository = new PostRepository();
        $this->postLimitChecker = new PostLimitChecker();
    }

    public function __invoke(
        User $user,
        Post $post
    ): void
    {
        if ($this->postLimitCheckerAction->isOverLimit($user)) {
            throw new Exception('投稿数の上限を超えています');
        }

        $this->postRepository->save($post);
    }
}

PostStoreActionには次のような問題があります。

  • 依存しているオブジェクトを考慮したテストが必要となり、リポジトリの実装が変更された場合、テストも再作成する必要が生じます。
  • 依存しているオブジェクトの実装が変更された場合、それに伴いPostStoreActionにも影響が及ぶ可能性があります。

これらの問題は、IoCコンテナを使用することで解決できます。IoCコンテナを利用すると、依存関係の注入が外部から行われ、クラスの疎結合が促進されます。結果として、テストの容易さが向上し、コードの柔軟性が高まります。

疎結合を実現するために

疎結合を実現するためには、どのようなアプローチを取るべきでしょうか?
一つの有効な方法は、依存しているオブジェクト間で共通のインターフェイスを定義し、依存関係をインターフェイスに向けることです。

従来の依存関係は以下のようになっていました。

これを、次のように変更します。

このアプローチによって、PostStoreActionは具体的なPostRepositoryPostLimitCheckerの実装ではなく、それらのインターフェイスに依存するようになります。

サービスコンテナについて

前述のIoCコンテナに続いて、Laravelのサービスコンテナについて詳しく説明します。
サービスコンテナはLaravelにおけるIoCコンテナの実装であり、アプリケーションの依存性管理を効率的に行います。

依存関係の解決

Laravelのサービスコンテナでは、インターフェイスと実装の依存関係を解決するために、bindsingleton、およびmakeを用いることが可能です。
これらのメソッドは、サービスコンテナにおいて異なる役割を果たします。

bindの使用

bindメソッドは、サービスコンテナからインターフェイスが解決されるたびに、新しいオブジェクトのインスタンスを生成します。これは、リクエストごとに異なる状態のインスタンスが必要な場合に適しています。

$this->app->bind(
    \App\Contracts\PostRepositoryInterface::class,
    \App\Repositories\Post\EloquentRepository::class
);

singletonの使用

一方、singletonメソッドは、最初に解決されたときにのみインスタンスを生成し、その後のすべてのリクエストで同じインスタンスが再利用されます。これは、アプリケーション全体で一貫性のある状態や設定を共有する場合に適しています。

$this->app->singleton(CacheInterface::class, FileCache::class);

makeの使用

makeメソッドは、サービスコンテナからクラスインスタンスを解決する際に使用されます。これは、アプリケーションの実行時に特定のクラスのインスタンスを生成するために便利です。

$postRepository = app()->make(\App\Contracts\PostRepositoryInterface::class);

実装例

インターフェイスの実装

まずはPostRepositoryインターフェイスの実装を行います。
このインターフェイスは、投稿データを保存するためのメソッドを定義します。

<?php

declare(strict_types=1);

namespace App\Models;

use App\Models\Post;

interface PostRepository
{
    public function save(Post $post): void;
}

次に投稿数の上限値は、設定情報を提供するサービスから取得するとします。
そのため、投稿数の上限値を提供する設定情報サービスのインターフェイスを定義します。

<?php

declare(strict_types=1);

namespace App\Models;

interface ConfigurationService
{
    public function getPostLimit(): int;
}

実装クラス

PostRepositoryの実装クラスは次のようになります。ここでは、Eloquentを利用して投稿データを保存します。

<?php

declare(strict_types=1);

namespace App\Repositories\Post;

use App\Models\Post;
use App\Models\PostRepository as PostRepositoryInterface;

class EloquentRepository implements PostRepositoryInterface
{
    public function save(Post $post): void
    {
        $post->save();
    }
}

ConfigurationServiceの実装は以下の通りです。

<?php

declare(strict_types=1);

namespace App\Services\Configuration;

use App\Models\ConfigurationService as ConfigurationServiceInterface;

class ConfigurationService implements ConfigurationServiceInterface
{
    public function getPostLimit(): int
    {
        return 10;
    }
}

PostLimitCheckerActionConfigurationServiceに依存していますが、インターフェイスを使用せず、具体的な実装クラスに直接依存するとします。

<?php

declare(strict_types=1);

namespace App\Service\User;

use App\Models\ConfigurationService;
use App\Models\User;

class PostLimitCheckerAction
{
    public function __construct(
        private ConfigurationService $configurationService
    ) {
    }

    public function isOverLimit(
        User $user,
    ): bool
    {
        return $user->posts->count() > $this->configurationService->getPostLimit();
    }
}

依存性の解決は、AppServiceProviderregisterで行うとします。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(
            \App\Models\PostRepository::class,
            \App\Repositories\Post\EloquentRepository::class
        );

        $this->app->bind(
            \App\Models\ConfigurationService::class,
            \App\Services\Configuration\ConfigurationService::class
        );

        $this->app->bind(
            \App\Service\Post\PostLimitCheckerAction::class,
            function ($app) {
                return new \App\Service\Post\PostLimitCheckerAction(
                    $app->make(\App\Models\ConfigurationService::class)
                );
            }
        );
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        //
    }
}

着目してほしいのが、PostLimitCheckerActionの依存解決です。
PostLimitCheckerActionの依存関係解決には、makeメソッドを使用してConfigurationServiceのインスタンスを取得しています。

投稿アクションクラスとコントローラー

PostStoreActionクラスは、投稿データの保存と投稿数の上限チェックを行います。

<?php

declare(strict_types=1);

namespace App\Service\Post;

use App\Models\Post;
use App\Models\User;
use App\Models\PostRepository;
use App\Service\Post\PostLimitCheckerAction;
use Exception;

class PostStoreAction
{
    public function __construct(
        private PostRepository $postRepository,
        private PostLimitCheckerAction $postLimitCheckerAction
    ) {
    }

    public function __invoke(
        User $user,
        Post $post,
    ): void
    {
        if ($this->postLimitCheckerAction->isOverLimit($user)) {
            throw new Exception('投稿数の上限を超えています');
        }

        $this->postRepository->save($post);
    }
}

コントローラーでは、次のように呼び出すことが可能になります。

<?php

namespace App\Http\Controllers\Api\Post;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class StoreController extends Controller
{
    public function __invoke(
        Request $request,
        \App\Service\Post\PostStoreAction $postStoreAction
    ): \Illuminate\Http\JsonResponse
    {
        // コントローラーの実装...
    }
}

まとめ

この記事では、Laravelの依存性管理の核とサービスコンテナについて掘り下げました。IoCコンテナとDI(依存性注入)の基本原則から始め、疎結合を実現する方法について解説しました。

具体的な実装例を通じて、インターフェイスの実装方法や、投稿アクションクラスとコントローラーの実際の使用法を見てきました。これにより、実際のアプリケーション開発においてLaravelのサービスコンテナをどのように活用できるかの理解を深めることに役立てていただけると嬉しいです。

GitHubで編集を提案

Discussion