Laravel / PHP ·

Laravelを使うが、ビジネスロジックを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 や読み取りまで同じ層を強制しない

本記事の設計判断・事例・技術的見解は筆者によるものです。構成・文章編集には生成AIを利用しています。