リポジトリパターンと Laravel アプリケーションでのディレクトリ構造


DMMグループ Advent Calendar 2019 4日目の記事です。

この記事では、弊チームの一部プロダクトで採用したリポジトリパターンについて紹介します。

リポジトリパターンについて

リポジトリパターンとはビジネスロジックとデータ操作のロジックを分離し、データ操作を抽象化したレイヤに任せるデザインパターンのことです。
リポジトリパターンでは、DBの操作や外部APIによるデータ取得等のデータソースへのアクセス部分は Repository インターフェースから完全に隠蔽されます。
そのため、アプリケーションはデータソースがDBであっても外部API静的ファイルであっても、それを意識することなくデータ操作を行うことができます。

ディレクトリ構造

今回採用したディレクトリ構造の例です。

app
|--Console
|--Exceptions
|--Http
| |--Controllers
| |--Middleware
| |--Requests
|--Models
| |--Article.php
| |--ArticleImage.php
| |--User.php
|--Providers
| |--AppServiceProvider.php
|--Repositories
| |--Article
| | |--ArticleRepository.php
| | |--EloquentArticleRepository.php
| |--User
| | |--UserRepository.php
| | |--DummyUserRepository.php
| | |--UserAPIRepository.php
|--Services
| |--ArticleService.php

Model クラスはデフォルトの場合 app 直下に設置されますが、Model が増えた際に視認性が悪くなるため、今回は models/ 配下に配置しています。

以下で各層の役割を説明します。

Service

この構造では Repository と Controller の間に、ビジネスロジックを管理するための Service を追加しています。
複雑なビジネスロジックを Service に切り出し、 Repository はデータの操作のみを行うように責務を分離することで、 Repository が複雑になることを防ぎます。

また、今回の場合だと Controller は外部からと外部への値の受け渡しのみを担っています。
しかし、ビジネスロジックがそれほど複雑でない場合は、 Service 層を実装せずに Controller に実装する方法でも良いかもしれません。

Repository

Repository 層は1つのインターフェースと1つ以上の Repository の実態で構成されています。

|--Repositories
| |--User
| | |--UserRepository.php
| | |--DummyUserRepository.php
| | |--UserAPIRepository.php

リポジトリパターンを使用することで、「特定の環境時は外部のAPIへの参照をダミーデータに変更する」「使用するDBを変更する」といった場合に対応しやすくなります。

例として、「通常は外部のAPIからユーザデータを取得し、テスト時はダミーデータを取得したい」という場合を考えてみます。

UserRepository はインターフェイスです。

UserRepository.php
interface UserRepository
{
    public function findUserByToken(string $token): User;
}

UserAPIRepository では外部のAPIから取得したデータを返しますが、 DummyUserRepository ではダミーデータを返すような作りにします。

UserAPIRepository.php
class UserAPIRepository implements UserRepository
{
    public function findUserByToken(string $token): User
    {
        try {
            $user = $this->getUser($token); // 外部APIからユーザー情報を取得する処理(省略)
            return $user;
        } catch(Exceptions $e) {
            throw new NoUserException();
        }
    }
}
DummyUserRepository.php
class DummyUserRepository implements UserRepository
{
    public function findUserByToken(string $token): User
    {
        $dummyUser        = User(); // Eloquent モデルではないただのクラス
        $dummyUser->id    = 1;
        $dummyUser->name  = 'ほげほげ';
        $dummyUser->token = 'abcd1234';

        if ($dummyUser->token === $accessToken) {
           return $dummyUser;
        }
        throw new NoUserException();
    }
}

AppServiceProvider で環境ごとに注入する Repository を変更します。

AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        // テスト環境の場合はダミーデータ用の Repository に向ける
        if (App::environment('testing')) {
            $this->app->bind(UserRepository::class, DummyUserRepository::class);
        } else {
            $this->app->bind(UserRepository::class, UserAPIRepository::class);
        }
    }
}

これにより、テスト環境のときはデータの取得方法を変更することができます。

このようにインターフェースを介すことで、実際の操作方法を意識することなくデータの操作を行うことができます。

Repository と Model

Model と Repository は 1:1 とは限りません。

例えば、以下のような要件があったとします。

  • 「記事( articles )」には必ず「記事の作者であるユーザ( users )」がおり、記事の削除・更新は作者のみ可能
  • 「記事( articles )」には「画像( article_images )」が紐付いており、この画像は全くない場合もあれば複数ある場合もある

仮に、Article クラスと ArticleImage クラスに対してリポジトリ、 ArticleRepository と ArticleImageRepository を実装したとします。

「画像( article_images )」は「記事の作者であるユーザ( users )」を知らないため、画像を削除・追加しようとした場合に「記事( articles )」を経由して「作者( users )」を取得するようなロジックを ArticleImageRepository に記述する必要があります。
もちろんこのロジックは ArticleRepository にも必要なため、データの整合性を担保するためのロジックが複数の Repository に分散してしまうことになります。
これを防ぐために、関連するオブジェクト郡( Article と ArticleImage )を1つの塊として考えて、両方のオブジェクトを扱う ArticleRepository を実装します。

関連するオブジェクトを集約することで、ロジックの分散を防ぎつつデータの整合性を担保することができます。

|--Models
| |--Article.php
| |--ArticleImage.php
| |--User.php
|--Repositories
| |--Article
| | |--ArticleRepository.php
| | |--EloquentArticleRepository.php

おわりに

この記事では、弊プロダクトで採用したリポジトリパターンを紹介しました。

以下、まとめです。

  • リポジトリパターンを使用することで、アプリケーションはデータの操作方法を意識することなくデータ操作を行うことができる
  • Repository には複雑なビジネスロジックを記述せず、データの操作のみ行うよう責務を分離することで Repository の複雑化を防ぐ
  • 関連するオブジェクト( Model )は1つの塊として考え、1つの Repository に集約する

この記事を通してどなたかのお役に立てれば幸いです!

明日の DMMグループ Advent Calendar 2019 の担当は、 @arika_nashika さんです!