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

## はじめに

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

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

標準化を進めるにあたって、目的としたのは以下のものです。
- ユニットテストを楽に書きたい
- 外部サービスなどは流用しやすいようにモジュール化
- Webページ(blade)とAPIで同じロジックを共有したい

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

## 前提

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


## そうしてできた構成図がこれ
（※説明のため、若干簡略化しています。）
（※あまりUMLに慣れていないので、間違っているかもしれません。）

```plantuml
node アプリ as app
node "Webサイト" as webpage

cloud internet

node php {
	node Laravel {
		node Middleware {
			[認証]
			[認可]
		}
		component Controller {
			[WebController]
			[APIController]
		}
		[Request]

		note right of Request {
			Requestの内容を参照するのは
			Controllerまで
		}
		
		frame view {
			[Blade]
		}
		frame BussinesLogic {
			[Usecase]
			frame Data {
				[Repository]
				note right of Repository {
					概ね論理データモデルの単位で作成する
				}
				[Model]
			}

		}
	}

	[Service]
	note top of Service {
		・業務システムに特化しない汎用的な機能
		・各種外部サービスとのインタフェース
	}
}


database MySQL

frame 外部サービス {
	cloud "・認証バックエンド\n・決済サービス\n・SNSサービス\n・ストレージサービス\nなど" {
	}
}

app -> internet
webpage -> internet

internet --> Middleware
APIController ---> Usecase
WebController ---> Usecase
Middleware -d--> APIController
Middleware -d--> WebController
認証 ---> Service
APIController .u.> Request
WebController .u.> Request
WebController ..> Blade
Usecase --> Repository
Usecase -d-> Service
Repository --> Model
Model -d-> MySQL
Service -d--> 外部サービス
```

ディレクトリ構造は以下のようになりました。
```
|--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で実装するのかは、判断が難しいところです。
ここも、判断のルールを策定して、検討時間の短縮を試みたいと考えてます。

## 最後に

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