Laravelでクリーンアーキテクチャ (ディレクトリ構造編)

トラストバンクふるさとチョイス事業本部CTO室に所属の海塩(うしお)と申します。
主に新規プロジェクトの立ち上げやアーキテクチャ設計を行っております。

前置き

PHP・Laravelにおけるクリーンアーキテクチャ実践の一例をご紹介します。
クリーンアーキテクチャ自体の説明はしておりませんのでご了承ください。
すでになんらかの形でドメイン駆動やクリーンアーキテクチャを実践している方に刺されば幸いです。
全体ボリュームがかなり大きいので、今回はディレクトリ構造のお話です。
今後は、

などを予定しています。

ディレクトリ全体像

こんな感じになります。

├── app
│   ├── Console
│   ├── Contexts
│   │   └── {コンテキスト名}
│   │       ├── Domain
│   │       │   ├── Entity
│   │       │   │   └── {エンティティ名}
│   │       │   ├── Exception
│   │       │   ├── Notification
│   │       │   └── Persistence
│   │       ├── Infrastructure
│   │       │   ├── Http
│   │       │   │   ├── Controller
│   │       │   │   └── Request
│   │       │   ├── Notification
│   │       │   │   └── templates
│   │       │   ├── Persistence
│   │       │   └── Presenter
│   │       │       └── templates
│   │       └── UseCase
│   │           └── {リソース名}
│   │               └── {アクション名}
│   ├── Exceptions
│   ├── Http
│   ├── Models
│   ├── Policies
│   ├── Providers
│   └── View
├── bootstrap
├── config
├── database
├── public
├── resources
├── routes
│   └── contexts
│       └── {コンテキスト名}
├── storage
├── tests
└── vendor

なるべくLaravel標準に従うことで、

  • プロジェクト参画者のキャッチアップがしやすくなる
  • Laravelのバージョンアップに低コストで追従できる

といったメリットが得られます。

app/Contexts

コンテキストを表現するディレクトリになります。
コンテキスト毎にレイヤー構成を持ち、コンテキストをまたいだ参照は一切無いようにします。

app/Contexts/{コンテキスト名}/Domain

クリーンアーキテクチャの「Enterprise Business Rules」と「Interface Adapters」にあたります。 ビジネスロジックがここに集まるようにします。
Laravelに関連するコード(グローバルメソッドやファサードも含む)は置いてはいけません。

app/Contexts/{コンテキスト名}/Domain/Entity

エンティティを配置します。
エンティティに紐づく値オブジェクトはサブディレクトリに配置します。

app/Contexts/{コンテキスト名}/Domain/Exception

コンテキストのビジネスロジックにより発生する例外を配置します。

app/Contexts/{コンテキスト名}/Domain/Notification

ビジネスロジックにおいて、何らかの通知を行う場合のインターフェースを配置します。
UserStoredNotificationみたいな名前で、handleメソッドのみを持つインターフェースであることが多いです。

app/Contexts/{コンテキスト名}/Domain/Persistence

エンティティの永続化、および復元に関連するインターフェースを配置します。
以下のセットをリソース単位で用意することが多いです。

  • UserRepository:単一エンティティの永続化と復元を行うインターフェース
  • UserListRepository:エンティティの一覧の復元を行うインターフェース
  • UserRepositoryRecord:エンティティとリポジトリとの間で使用するDTOインターフェース

app/Contexts/{コンテキスト名}/Infrastructure

クリーンアーキテクチャの「Frameworks and Drivers」にあたります。
ビジネスロジックの実現手段をここに配置します。

app/Contexts/{コンテキスト名}/Infrastructure/Http

Web経由でのコントローラを配置します。
LaravelのControllerやFormRequestを活用します。
本家クリーンアーキテクチャと大きく動きが異なります。
コントローラはユースケースを呼んで終わりではなく、戻りを受け取ります。
さらにユースケースからの戻りをプレゼンターへ渡してUIを生成し、Webのレスポンスとして返却します。

app/Contexts/{コンテキスト名}/Infrastructure/Notification

Domain/Notificationのインターフェースの実装を配置します。
メール通知であればLaravelのMailableやSendGrid APIを活用します。

app/Contexts/{コンテキスト名}/Infrastructure/Persistence

Domain/Persistenceのインターフェースの実装を配置します。
データベースへの永続化であればLaravelのEloquent Modelを活用します。

app/Contexts/{コンテキスト名}/Infrastructure/Presenter

Web経由でUIを伝えるための実装を配置します。
クリーンアーキテクチャの「Interface Adapters」にあたりますが、上述の通り構成はだいぶ異なります。
同じレイヤのコントローラから呼ばれて戻り値としてUIを返すことになるため、インターフェースと実装は分離しません。

app/Contexts/{コンテキスト名}/Infrastructure/Presenter/templates

コンテキスト固有のBladeテンプレートを配置します。
layoutや共通パーツなどはLaravel標準のresources/viewsに置きます。 \Illuminate\Support\Facades\View::addNamespaceによって動的にディレクトリを設定するため、configやLaravelコードはいじりません。

app/Contexts/{コンテキスト名}/UseCase

クリーンアーキテクチャの「Application Business Rules」にあたります。
Domainコードを活用し、ビジネスロジックを組み立てます。
ここにもLaravelに関連するコード(グローバルメソッドやファサードも含む)は置いてはいけません。

終わりに

いかがでしたでしょうか。
クリーンアーキテクチャの実践には正解がなく、日々改善を重ねています。
トラストバンクでは、堅牢で変更に強いプログラムを書きたいエンジニアの方を募集しています!

www.wantedly.com