Laravel / PHP ·

Repository InterfaceとDTOでEloquentから業務ロジックを切り離す

  • Laravel
  • Eloquent
  • Repository

この記事で扱う問題

Eloquent のモデルは便利です。ただ、業務ロジックがモデルを直接受け取るようになると、次のことが起きます。

  • ロジックの途中でリレーションを読むと、そこで SQL が走る(N+1 がロジックの中に潜む)
  • どのカラムが読み込まれているかが呼び出し経路次第で変わり、null の意味が曖昧になる
  • ロジックのテストに DB とファクトリが必須になる
  • $model->save() をロジックのどこからでも呼べてしまう

つまり、永続化の都合がロジックに漏れ出している状態です。

判断

業務ロジックが使う入口を Repository Interface として定義し、Eloquent はその実装の中に閉じ込めます。

interface OrderRepository
{
    public function findOrFail(OrderId $id): Order;

    public function save(Order $order): void;
}

final class EloquentOrderRepository implements OrderRepository
{
    public function findOrFail(OrderId $id): Order
    {
        $row = OrderModel::with('items')->findOrFail($id->value);

        return OrderMapper::toDomain($row);   // Eloquent モデル → ドメインのオブジェクト
    }

    public function save(Order $order): void
    {
        $model = OrderModel::findOrNew($order->id()->value);   // 既存の行なら UPDATE、新規なら INSERT
        OrderMapper::fill($model, $order);                     // ドメインのオブジェクト → Eloquent モデル
        $model->push();
    }
}

層をまたいでデータを渡すときは、Eloquent モデルではなく DTO(ただの値の入れ物)を使います。

final readonly class OrderSummary
{
    public function __construct(
        public string $id,
        public string $status,
        public int $totalAmount,
    ) {
    }
}

なぜそう判断したか

ポイントは、ロジックが必要とするものを、ロジックの言葉で定義することです。

  • findOrFail(OrderId) は「注文を1件取り出す」というドメインの操作で、SQL やリレーションの読み込み方は実装側の責任になります
  • ロジックのテストでは、メモリ上の Fake Repository を使えます。DB なしで業務ルールだけを速く検証できます
  • DTO は読み込まれている値が型で決まっているので、「この値は読み込まれていないのか、本当に null なのか」という曖昧さがなくなります

選ばなかったのは、「Eloquent モデルをドメインモデルとして使う」やり方です。Active Record は小さな規模では合理的ですが、業務ルールと永続化の変更理由が別々になってくると、1つのクラスに2つの理由で手が入り続けます。

どこから設計過剰になるか

この構成は、マッピングのコードが増えるのが明確なコストです。

  • 単純な CRUD で業務ルールがほとんどない機能では、Repository を挟む利点がほぼありません
  • 一覧・検索などの読み取りは、Repository を経由せず、専用のクエリクラスから DTO を直接返すほうが素直です(CQRS 的な分け方)
  • 「将来 DB を変えるかもしれないから」だけを理由にするのは弱いです。本当の利点は、ロジックのテスト容易性と、変更理由の分離にあります

まとめ

  • Eloquent モデルを業務ロジックに渡すと、永続化の都合がロジックに漏れる
  • Repository Interface はドメインの言葉で定義し、Eloquent は実装に閉じ込める
  • 層の間は DTO で渡し、読み取りは専用のクエリクラスで十分なことが多い

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