207
181

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DMMグループAdvent Calendar 2019

Day 4

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

Last updated at Posted at 2019-12-04

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 )」が紐付いており、この画像は全くない場合もあれば複数ある場合もある

er.png

仮に、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 さんです!

207
181
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
207
181

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?