CTOの名人です。
本記事では、マナリンクで採用しているLaravelのディレクトリ構造を晒そうと思います。
ディレクトリ構造って案外個性が出ますよね。Webアプリケーションの果たしている役割はHTTPリクエストの分解から、そのバリデーション、サービスの仕様となるビジネスロジックの実現、データストアへの永続化、複数種類(ユーザー起因、システム起因)のエラー対応など多岐にわたります。これらのソースコードをどう配置しているかは、開発に対する考え方を一部投影したものになっているはずです。
それではさっそくプロジェクトルートから順に解説していきます。
環境情報
- Laravel 6系
- PHP 7.4系
プロジェクトルート
$ tree -L 1 -d .
├── docker
├── node_modules
├── src
├── src-node
└── uml
- README
- docker-compose.yml: ローカル開発で使います
- src: Laravelのソースコードが入っています
- uml: ドメインモデル図だったり、インフラ構成図が入っています。plantUMLで書いていて、吐き出した画像も一緒に入れています
- node_modules: commit時にgit-huskyを動かしているので、そのライブラリがインストールされています
- src-node: 諸事情でNestJSで一部のAPIを書いているので、そのAPIはこっちのディレクトリに入っています
Laravelのルート
$ tree -L 1 -d src
├── app
├── bootstrap
├── config
├── database
├── public
├── resources
├── routes
├── storage
├── tests
└── vendor
- いわずもがな。定番のLaravelのルートディレクトリです
/app
$ tree -L 1 -d src/app
src/app
├── Console
├── Constant
├── Domain
├── Events
├── Exceptions
├── Helpers
├── Http
├── Listeners
├── Logging
├── Mail
├── Models
├── Notifications
├── Policies
├── Providers
├── Repositories
├── Services
└── ValueObject
- Console, Events, Exceptions, HttpといったLaravelオリジナルのディレクトリについては割愛します
Domain
: このディレクトリ以下にDDDの設計指針に準拠した主な処理(ユースケース、リポジトリ、ValueObjectなど)を書いています。詳しくは後述Helpers
: Firebase SDKのラッパーといった外部モジュールをラップしたものや、LaravelのCollectionに関するオリジナルの変換メソッドとかを入れています。ニュアンスとしてはドメイン知識に関係ないモジュールの集まりといった感じだが、Helperという命名が個人的には微妙に感じているので、なんか代案を考えたいです。Libとか?Repositories、Services: マナリンクの前身となるNoSchoolというサービスの時代はシンプルなレイヤードアーキテクチャを使っていたときの名残。今はメインのロジックを全部Domain/以下に置いているが、管理画面の一部処理や、サイトマップ生成のために脳死で全件取得する処理はこっちに残してい
ますValueObject: 今見てみたらめっちゃ昔DDDに興味を持ち始めた頃に作ったValueObjectクラスが1つだけぽつんと入っていました。そのクラスは近日中に消す予定のごく一部の機能で使われているのでそれに伴って削除予定
/app/Domain
$ tree -L 2 -d src/app/Domain
src/app/Domain
├── Auth
│ ├── Context
│ ├── Domain
│ ├── Infrastructure
│ └── UseCase
├── Base
│ ├── Domain
│ ├── Exception
│ ├── Infrastructure
│ └── UseCase
...中略...
├── OnlineTeaching
│ ├── Domain
│ ├── DomainService
│ ├── Infrastructure
│ ├── Presentation
│ ├── UML
│ └── UseCase
├── Shared
│ ├── Domain
│ └── Presentation
...以下略...
DDDっぽくやっているので、いわゆる境界づけられたコンテキスト的なものがずらっと並び、その下にそれぞれDomain、Infrastructure、UseCaseといったディレクトリが並ぶ感じになっています。
Domainっていう命名にするとDomain\Hoge\Domainみたいなnamespaceになって若干ダサいですが、普通どうするものなのかわからない(サービス名のManalinkってするのは若干気が引ける)のでこのままにしています。
Domainディレクトリは以下のように、EntityとかValueObjectとか、Exception(ドメイン例外)とかを入れています。RepositoryにはRepositoryのInterfaceを入れている。実装クラスはInfrastructureの中に入れます。
src/app/Domain/SAMPLE_CONTEXT/Domain
├── Entity
├── Exception
├── Repository
└── ValueObject
Infrastructureディレクトリは以下の感じ。1つのコンテキストの中に、何種類もの機能やデータ(以下、FEATUREと表記する)が入ってくると、Infrastructure直下に大量のクラスが並び見通しが悪いので、FEATURE単位でディレクトリを切る方向で落ち着いています。UseCaseのほうも同等に機能別にサブディレクトリを切っていることが多いです。
src/app/Domain/SAMPLE_CONTEXT/Infrastructure
├── SAMPLE_FEATURE_1
├── SAMPLE_FEATURE_2
...以下略...
ちなみに、ここでいうFEATURE単位でコンテキストを切るのもありだとは思うし、以前そうしていたこともありました。しかしDDDにおける境界づけられたコンテキストの定義から言っても、互いにuseし合うことがわかっている関係であれば同じコンテキストに入れたほうが管理しやすい点で言っても、Domain直下の各コンテキストは気持ち大きめの粒度で切るのがおすすめです。
全体的に、DDDをやっているとはいえ、Entityにドメインロジックを入れることができたら個人的には満足です。それゆえLaravelからの依存を完璧に切り離そうなどとは思っていないし、事業的にトライアルフェーズの機能開発のときは手抜きでサクッと実装することもあるので、割と気楽にやっています。※テストコードさえ書けばあとからなんとでもなると思っているフシはある
/app/Notifications
$ tree -L 2 -d src/app/Notifications
src/app/Notifications
├── Base
├── Database
│ ├── SAMPLE_CONTEXT_1
│ └── SAMPLE_CONTEXT_2
├── Facades
├── Sender
└── Slack
├── SAMPLE_CONTEXT_1
├── SAMPLE_CONTEXT_2
└── SAMPLE_CONTEXT_3
ユーザー向けの通知は大きく分けて、データベースに保存してユーザーが通知一覧から確認するものと、メール通知があります。また、社内向けに、特定のアクション発生時にSlack通知をすることがあります。
将来的にAWSのSNS等を使ってみたいものの、まだそこまで手が回っていないのでLaravelでモノリシックにやっています。メール通知はMailディレクトリです。
Laravelでのメール通知がMail::sendだったことから、データベースへの通知保存やSlack通知もDatabase::save()やSlack::send()といった使い勝手に合わせてみましたが、FacadeよりInterfaceとサービスコンテナを使う方向性のほうがよかったかなと少し後悔している部分はあります(いずれAWS SNS等に切り替えたいならなおさら)。Facadeはテストコードが書きやすいメリットはありますが、逆に言えばテストコードまでFacadeにズブズブになると言うこともできます。
Mailディレクトリも同じなのですが、ポイントはサブディレクトリにDomain側のコンテキストと同じネーミングを使うことです。Laravelのディレクトリ構造に準拠しつつ、できるだけ目的のファイルを探しやすくする工夫です。
/app/Http/Controllers
$ tree -L 3 -d src/app/Http/Controllers
src/app/Http/Controllers
├── Api
│ ├── Admin
│ │ ├── [管理画面の各機能ごとにディレクトリ]
│ └── v1
│ ├── App
│ ├── SAMPLE_CONTEXT_[1-N]
├── Auth
└── Debug
- 基本的にLaravelはNuxtに対するAPIサーバーとして動かしているので、Apiディレクトリを切っています
- まだv2にしたことがないけど、一応v1というディレクトリを切っています
- 管理画面系のControllerはAdminというディレクトリを切っています
- 一部、React NativeアプリまたはFirebase functionsから呼ばれるAPIがあるので、そっちはAppディレクトリ以下にしています
ちなみに、ディレクトリ構造ではないですが、私はControllerは1アクション1クラスにしています。
以下は指導コースの詳細を取得するControllerの例です。※一部割愛
class GetTeachingCourseDetailController extends \App\Http\Controllers\Controller
{
public function __invoke(int $id, GetTeachingCourseDetailUseCase $findTeachingCourse)
{
$course = $findTeachingCourse->execute($id);
return new TeachingCourseResource($course);
}
}
__invokeメソッドのみを定義することで、Routingファイルから書く時に::classを指定するだけで便利になります。あとFat Controllerにもなりにくいです。
Route::get('/{id}', GetTeachingCourseDetailController::class)->where('id', '[0-9]+');
こう書くとIDEから参照も簡単に追えます。
/tests/
最後に簡単にテストディレクトリについても解説します。
$ tree -L 2 -d src/tests (git)-[feature/delete-files]
src/tests
├── Feature
│ ├── SAMPLE_CONTEXT_[1-N]
├── Fixture
│ └── SAMPLE_CONTEXT_[1-N]
├── Lib
└── Unit
├── SAMPLE_CONTEXT_[1-N]
...以下略...
テストコードのディレクトリはこんな感じです。
- Feature: 結合テスト
- Fixture: テスト用のデータ
- Lib: テスト用の基底クラス
- Unit: 単体テスト
テストコードのディレクトリもDomain側のコンテキスト名と合わせるようにしています(一部追いついていないところがあるけど)。
余談
以前私がQiitaでバズった設計に関する記事を置いておきます!
ご主人様、小難しいDDDやクリーンアーキテクチャはお忘れになって、”削除しやすい設計”から始められてはいかが?
https://qiita.com/mejileben/items/48473a572ec07cbaf65f
まとめ
Laravelのディレクトリ構造を公開してみました。この記事を読んだ方はぜひご自身のディレクトリ構造も晒してください!
ディレクトリ構造の公開は、設計思想を伝えることができ、かつセキュリティ的にそこまで痛い内容にもならないので、ネタとして結構面白い気がしています。
今後、NuxtやReact Nativeのディレクトリ構造を公開するのにも取り組んでいこうと思います。
オンライン家庭教師マナリンクではWebエンジニア、React Nativeエンジニアを募集しています。
この記事を読んで興味を持った方はぜひお話しましょう!入社試験を作っているので、色んな方に問いてみてほしいです。何卒よろしくお願いします。