75
61

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 1 year has passed since last update.

Laravelの汎用的な構成を考えてみた

Last updated at Posted at 2022-07-14

はじめに

弊社では、バックエンドにLaravelを使うことが多いです。
Laravelは非常に生産性が高いと考えてます。

しかし、さらに生産性を上げるべく、モジュール構成の検討を進めています。

標準化を進めるにあたって、目的としたのは以下のものです。

  • ユニットテストを楽に書きたい
  • 外部サービスなどは流用しやすいようにモジュール化
  • Webページ(blade)とAPIで同じロジックを共有したい

この記事は、これを目指して、試行錯誤している内容となります。

前提

  • Laravelの機動性の高さを活かすべく、過度に凝ったモジュール構成をしない
  • コーディング量が大幅に増えない範囲で検討する
  • 実装にはInfyOmLaravelGenelatorを使用する前提とする
    (「とりあえず動く」状態からの変更は非常に生産性が高いので)

そうしてできた構成図がこれ

(※説明のため、若干簡略化しています。)
(※あまりUMLに慣れていないので、間違っているかもしれません。)

ディレクトリ構造は以下のようになりました。

|--app
|  |--Usecases
|  |--Console
|  |--Consts
|  |--DTOs
|  |--Exceptions
|  |--Http
|  |  |--Controllers
|  |  |  |--API
|  |  |  |  |--V1
|  |  |--Middleware
|  |  |--Requests
|  |  |  |--API
|  |  |  |  |--V1
|  |--Models
|  |--Providers
|  |--Repositories
|  |--Rules
|  |--Services
|  |--Usecases
|  |--View
|--config
|--database
|  |--factories
|  |--migrations
|  |--seeders
|--public
|--routes
|--storage

各モジュールの説明と実装時のルール

Controller

  • ログインユーザーを意識するのはこのControllerまで
  • Webサイト用のWebControllerとアプリやWebアプリからのリクエストを受け付けるAPIControllerそれぞれ作成する
  • 特殊な場合のを除きTransactionの制御もControllerで行う
  • エラーのCatchを行ない、適切なhttpステータスにして返却する

WebController

  • Webサイトの場合に実装する
  • Bladeを使用してレンダリング後の結果を返却

APIController

  • APIの場合に実装する
  • JSONを返却

Request

  • 基本的にはRouteの単位で作成する
  • パラメータがない場合は、作成しない
  • バリデーションルールを定義する

Usecase

  • 対象のシステムに依存する処理を書く
  • Webサイトと、APIで共通化しても良い(むしろ共通化したい)
  • ビジネスロジックを記載する
  • メソッドは引数以外の情報(セッションやリクエストなど)は使用しない
    • メソッドは引数の値以外の要因で結果が変わらないようにする
  • UnitTestを作成する
  • 検索、詳細データ取得系の返却はPaginatorが望ましい
    • 0〜1件を返却するような詳細データ取得でもPaginatorを返却する
    • そうすることで、検索条件のみの変更で同じ処理を流用しやすくなる

Repository

  • 論理データモデルの単位でClassを作成する
  • メソッドは検索方法や更新方法ごとに作成する
  • Joinもここで吸収する

Model

  • テーブル(物理データモデル)の単位で作成する

Service

  • 対象のシステムに依存しない共通処理を書く
  • 外部サービスとのインタフェースもServiceで処理する
  • 他サービスでも利用できるようにするのが望ましい
  • テーブルへのアクセスが必要な場合、そのテーブルも業務に依存しないようにする

目的をどのように達成したのか

ユニットテストを楽に書きたい

Controllerを含むユニットテストは出力がHTMLだったり、JSONだったりして、非常に作成しづらいです。
また、Requestをテストのパラメータとして与えるのも実装に時間がかかります。
RequestとResposeをControllerの責務として、それ以外の処理をUsecaseとして作成することで、ユニットテストを実施しやすくなります。

外部サービスなどは流用しやすいようにモジュール化

Service(および、ServiceProvider)に外部サービスとのインタフェースを実装します。
UsecaseではServiceを使用するのみとして、外部サービスとの結合度を下げます。
結合度が下がることで、再利用しやすくなり、また、外部サービスへの依存度も下がります。

Webページ(blade)とAPIで同じロジックを共有したい

管理画面などbladeで表示する一覧と、APIで取得する一覧は同じ処理で取得できることが多いです。
検索条件や出力の項目が異なるだけのことが多く、これらの違いは、UsecaseのパラメータやControllerでのレスポンスの編集で吸収できます。
検索条件は、「絞り込み」として表現できることが多く、可変パラメータや検索条件のDTOを実装することで、パラメータの差異に対応できます。
(DTOについては、上記の図には記載していません。)

実際やってみて苦労したところ

Usecaseにクエリを書いてしまう

一番苦労したのはRepositoryの扱い、どこまでRepositoryに任せるかという点。
現状の実装の結果としては、ほぼUsecasaeでJOINのクエリを書いてしまったのでRepositoryパターンの良さを活かしきれませんでした。
この部分は今後の課題として対処していきます。

今後の課題

Usecaseには、かなりの数のメソッドを実装することになりますが、このメソッドのルールをあまり決められなかったです。
パラメータのルールを統一できれば、ソースの共通理解ができて、生産性や保守性をさらに上げることができそうです。

あとは、上記のRepositoryをJOINを含むクエリ問題です。
データ取得のパターンは無限に出てくるので、全てRepositoryに実装するのか、Usecaseで実装するのかは、判断が難しいところです。
ここも、判断のルールを策定して、検討時間の短縮を試みたいと考えてます。

最後に

このモジュール構成も、私達の実装技術もまだまだ発展途上です。
詳しい方のアドバイス等をいただけると幸いです。

75
61
1

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
75
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?