LaravelでDDDをやるときの命名と配置の落とし所
LaravelでDDDをやろうとすると、「UseCase?Repository?どこに置けば?」と最初に迷います。
自分もいろいろ試して、ようやく“現実的に破綻しない落とし所”が見えてきました。
目次
まず、分け方は4層+αくらいでちょうどいい
DDDをそのままLaravelに持ち込むと、抽象クラスやインターフェースだらけになって逆に見失いがちです。実務では、次のくらいの分け方に落ち着きました。
Domain ← 業務のルール(エンティティ・例外・辞書)
Application ← UseCase・DTO・Command/Result
Infrastructure ← DBや外部APIなどの実装
Presentation ← Controller・Request・ViewModel
Support ← Formatterや共通ヘルパLaravelのレイヤ構造を壊さず、役割ごとに責務を1つに絞るのがポイント。
命名は「動詞+名詞」でそろえる
「何をするか」が先、「何に対して」が後。
これでファイルツリーを見たときに、UseCaseが動作単位で並びます。
- UseCase:
SearchSpotUseCase,CreateSpotUseCase,PublishSpotUseCase - Repository:
SpotRepository(IF)/DbSpotRepository(実装) - Query:
SpotListQuery(IF)/DbSpotListQuery(実装) - 例外:
SpotNotFoundException,NewsNotFoundException
呼び出す順番をそのまま読めるようにしておくと、プロジェクトが大きくなっても迷いません。
Repository と Query の使い分け
- Repository … 「更新・削除・登録」など、ドメインの整合性を守る処理
- Query … 画面表示や検索などの「読み取り専用・最適化された処理」
Eloquentを使うのはInfra層だけにして、
DomainからはEloquentを一切見ないようにしておくと長持ちします。
DTOとViewModelの語尾を決めておく
これを最初に決めておくと混乱しません。
| 役割 | 命名 | 例 |
|---|---|---|
| 入力 | *Command / *Query | SearchSpotCommand |
| 結果 | *Result | SpotListResult |
| 行 | *Row | SpotListItemRow |
| 画面表示用 | *ViewModel | SpotListItemViewModel |
ViewModelは整形を持たず、フォーマットは別クラス(DateFormatterなど)に任せます。
Controllerでは「3行で完結」できる構造に
final class SpotController extends Controller
{
public function index(SpotSearchRequest $request, SearchSpotUseCase $uc)
{
$cmd = $request->toCommand();
$result = $uc->handle($cmd);
return view('spot.index', [
'vm' => SpotListViewModel::fromResult($result),
]);
}
}- Request:入力の正規化
- UseCase:業務処理
- ViewModel:表示用整形
この3点セットが自然に書ければ、設計はほぼ成功です。
例外はモジュールごとにハンドラを持たせる
共通の App\Exceptions\Handler を無理に拡張せず、
パッケージ内で GeneralExceptionHandler::register($handler) のように後付けする形が安全。
(詳しくは「Laravelでpackageを切るときの自作の例外ハンドラ」記事を参照)
実務で壊れにくいルール(最小)
- 動詞+名詞+UseCase を統一
- QueryとRepositoryを分ける
- DTOは配列で返さない(型を守る)
- EloquentはInfrastructureだけ
- nullを返さず空値で処理する
この5つを守るだけで、チーム開発でもほとんど迷わなくなります。
まとめ
DDDを“完璧にやる”よりも、Laravelらしく分ける方が長持ちする。
命名と配置を最初に決め切っておくと、あとから人が増えてもスッと馴染みます。
迷ったら「UseCaseは動詞から」「EloquentはInfraだけ」——
この2つを思い出せば大体うまくいくかなと言う感じですね。
今回は以上です!