Laravelを使うが、ビジネスロジックをLaravelに支配させない
この記事で扱う問題
Laravel は、少ないコードで動くものを作れるフレームワークです。Controller に処理を書き、Eloquent のモデルにメソッドを足し、Facade でどこからでもキャッシュやキューを呼べる。最初の数か月は、これが一番速い書き方です。
ところが、機能が増えて数年たつと、次のような状態になりがちです。
- Controller が数百行になり、同じ判定があちこちの Controller に書かれている
- Eloquent モデルに、画面表示・業務ルール・外部連携が同居している(Fat Model)
- Facade 経由の呼び出しが深い場所に散らばり、どこで何が起きるか追えない
- モデルのプロパティをどこからでも書き換えられ、「この状態になるのはどの経路か」が分からない
こうなると、仕様変更のたびに影響範囲の調査が重くなり、テストも書きにくくなります。
判断
私は Laravel を使うとき、次のように責務を分けます。
Controller
↓ 入力を受け取り、UseCase を呼び、結果をレスポンスにする
UseCase / Application Service
↓ 1つの業務操作の手順を組み立てる(トランザクションの境界もここ)
Domain Service / Domain Model
↓ 業務ルールと状態遷移を守る
Repository(Interface)
↓ ドメインの言葉で永続化を表現する
Infrastructure(Eloquent・外部API・S3 など)
Controller は薄く保ちます。
final class ConfirmOrderController
{
public function __construct(private ConfirmOrder $useCase)
{
}
public function __invoke(ConfirmOrderRequest $request, string $orderId): JsonResponse
{
$result = $this->useCase->execute(new OrderId($orderId), $request->user()->id);
return response()->json(OrderResource::fromResult($result));
}
}
UseCase が知っているのは、ドメインのオブジェクトと、Repository・トランザクション管理・時計のようにフレームワークに依存しない抽象(Interface)だけです。Eloquent や Facade、now() は直接使いません。現在時刻も Clock から受け取るので、テストで時刻を固定できます。
final class ConfirmOrder
{
public function __construct(
private OrderRepository $orders,
private TransactionManager $transaction,
private Clock $clock,
) {
}
public function execute(OrderId $id, int $operatorId): ConfirmOrderResult
{
return $this->transaction->run(function () use ($id, $operatorId) {
$order = $this->orders->findOrFail($id);
$order->confirm($operatorId, $this->clock->now()); // 状態遷移のルールは Order が守る
$this->orders->save($order);
return ConfirmOrderResult::from($order);
});
}
}
Eloquent は Repository の実装の中だけで使います。
なぜそう判断したか
Laravel を否定しているわけではありません。ルーティング、バリデーション、DI コンテナ、キュー、マイグレーションなど、Laravel の機能は外側(Controller と Infrastructure)で積極的に使います。
分けたいのは、「業務として何が正しいか」というルールと、「それをどう保存し、どう通信するか」という手段です。
- ルールが Eloquent や Facade に依存していなければ、DB なしでルールのテストが書けます
- 保存方法(テーブル構成、キャッシュ、外部 API)を変えても、ルールの側は変わりません
- 「この状態変更はどこで起きるか」が UseCase と Domain に集まり、影響範囲を追いやすくなります
選ばなかった選択肢は、「Laravel の流儀に全面的に乗る」ことです。小さなアプリならこれが正解ですが、業務ルールが複雑で長く運用するシステムでは、フレームワークの都合がルールの形を決めてしまいます。
どこから設計過剰になるか
この構成には、ファイル数と間接参照が増えるというコストがあります。次のような場合は、層を減らします。
- 管理画面の単純な CRUD で、業務ルールがほとんどない
- 数か月で捨てる前提の PoC
- 読み取り専用の一覧・検索(Query 側)は、UseCase を挟まず専用のクエリクラスで十分なことが多い
全部の機能に同じ層を強制するのではなく、ルールが複雑な部分にだけ境界を引くのが現実的です。
まとめ
- Laravel の機能は外側で使い、業務ルールは Laravel に依存しない場所に置く
- Controller は薄く、UseCase が手順を、Domain がルールを、Repository が保存を担う
- 単純な CRUD や読み取りまで同じ層を強制しない