📦

LaravelでDTOを使う理由と設計ルール

に公開

LaravelでDTOを使う理由と設計ルール

はじめに

Laravelでは、リクエストやモデルのインスタンスをそのままServiceやControllerに渡して処理を進めることができます。
その柔軟さゆえに、つい次のようなコードを書いてしまうことも多いのではないでしょうか。

  • $request->input() をそのまま使って値を取り出す
  • 配列でデータを渡し、バリデーション済みかどうかが曖昧なまま処理する
  • 値の意味や責務が明確でないまま、各層を“なんとなく繋ぐ”

このような構造は、最初はシンプルで動きます。しかし、機能が増えたり、複数人で開発するようになると、
データの流れが見えづらい」「安全性が担保されない」「保守がつらい」という問題が表面化します。


そこで本記事では、LaravelにおいてDTO(Data Transfer Object)を導入する意義と、
私が実務で採用しているDTO設計ルールを紹介します。

  • なぜDTOを使うのか?
  • どんな場面で役立つのか?
  • Laravelらしい書き方とは?

これらを具体的なコード例とともに解説していきます。


よくある課題:データが“裸”のまま流れる

Laravelはとても柔軟なフレームワークなので、バリデーション後のデータをそのまま処理に渡しても動いてしまいます。

例えば、以下のようなコードを書いた経験がある方も多いのではないでしょうか?

public function update(Request $request)
{
    $request->validate([
        'title' => 'required|string',
        'description' => 'nullable|string',
    ]);

    $data = $request->only(['title', 'description']);

    Task::find($request->input('id'))->update($data);

    return redirect()->back()->with('success', '更新しました');
}

一見シンプル。でも何が問題?

このようなコードは見た目は短く、Laravelらしく書かれています。
しかし、以下のような “見えない課題” を抱えています。

  • ❌ バリデーション済みかどうかが呼び出し元から分からない
    • $request->only() で取り出された値が 安全な値かどうか はコードを追わないと分かりません。
  • ❌ 値の意味や型が明確でない
    • titledescription が「どんな条件で必要か」「型の保証があるか」などが不明瞭。
  • ❌ データ構造が“ふわっと”している
    • 配列で値を渡すと、あとから項目が増えたときに影響範囲が分かりづらくなります。

このように「動くけれど将来つらくなるコード」が増えていくと、

  • チーム開発で混乱が起きやすくなる
  • テストが書きづらくなる
  • バグの原因が追いにくくなる

といった課題に繋がります。


そこで登場するのが、DTO(Data Transfer Object) です。

DTOを導入するメリット

前のセクションで紹介したような「データが裸のまま流れていく」問題を解決するために、
積極的に取り入れていきたいのが DTO(Data Transfer Object) です。

DTOは、“バリデーション済みの値をひとまとめにして安全に運ぶ” ための専用クラスです。


✅ 1. データの入り口を明確にできる

DTOは FormRequest$request->input() から取得した値をコンストラクタで明示的に受け取ります
これにより、「この値がどこから来たか」「何を受け取っているか」が明確になります。

final class TaskUpdateDto
{
    private string $title;

    public function __construct(UpdateRequest $request)
    {
        $this->title = $request->input('title');
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}

✅ 2. 型の保証ができる

DTOクラスはプロパティに型を定義できるため、配列やRequestのような曖昧な構造ではなく、
「この値はstringである」などの型保証 をコード上で担保できます。

これはPHP 8.1+ 以降の強い型サポートとの相性も抜群です。

✅ 3. getter経由で安全に値を取り出せる

Laravelでは $request->input('title') のように値を直接参照する書き方が一般的ですが、
DTOでは getTitle() のような getterメソッド 経由に統一することで、
「どんな値が使われるか」「使い方が正しいか」 を読み手にも明確にできます。

✍️ 実務では getter 経由に統一することで、予期せぬ値の改変や流通経路の混乱を防いでいます。


✅ 4. Service・Model間のインターフェースが明確になる

DTOは「データを運ぶ役」に徹するため、Serviceとのやりとりが非常に明快になります。

public function handle(TaskUpdateDto $dto): void
{
    $task = new Task();
    $task->title = $dto->getTitle();
    $task->save();
}

Service側は「DTOから値を取り出して使う」ことに専念でき、
コントローラー → DTO → サービス → モデル という美しい流れが生まれます。


補足:DTOを導入するタイミングと、あとでどう使うかを見据えた設計

🔄 DTOを導入すべきタイミングとは?

DTOは最初から必ず必要というわけではありません。
むしろ、小規模な処理や一時的なスクリプトでは冗長になる場合もあります。

では、どんなときにDTOの導入を検討すべきでしょうか?

✅ DTO導入を検討すべきケース

  • Controllerで複数の入力値を扱い始めた
  • Serviceへの値の受け渡しが煩雑になってきた
  • 値の出どころが曖昧になりはじめた
  • チーム開発で「この値何?」という確認が増えてきた
  • テストコードを書くときに「値のセット」が煩雑になってきた

このような兆候が見え始めたとき、DTOの導入によって「構造の安定性」を得ることができます。

🔮 DTOは“再利用”と“変更”に耐えるための設計

DTOは「データを渡すためだけの一発使い捨ての構造」ではありません。
将来的な拡張や変更に備えた“耐久性のある設計” を意識して構築するのがポイントです。

DTOをあとで使うつもりで設計する

想定 設計配慮
テストで使う getter経由で明示的に値を取得できるようにする
ServiceやModelに再利用する 明確な責務分離+冗長な値は入れない
後から値を追加するかもしれない コンストラクタの引数や構成に拡張性を持たせる(可変長でなく、明示的に)
ログ出力や履歴保存で使う 表示用との境界を意識し、PreviewDtoなどと分離する

DTOを「今この1画面で使えればOK」と考えて作ると、後で「フィールド名が合わない」「責務が曖昧」「拡張に弱い」といった問題が起こりがちです。

DTOとは「将来的にも責務を果たし続けられる、小さなインターフェース」のようなものです。


このような観点を踏まえておくと、DTOを導入する判断や設計方針にも納得感が増し、
プロジェクトの拡張性・安定性が格段に高まります。

実装例とテンプレートでの構成

ここからは、実際にDTOをどう使っているのか、どのようにテンプレート構成に落とし込んでいるかを紹介します。


🧱 実装例:DTO → Service → Model の連携

1. Controller(入力とDTO生成)

public function __invoke(UpdateRequest $request): RedirectResponse
{
    $dto = new TaskUpdateDto($request);
    $this->service->handle($dto);

    return redirect()->route('tasks.index')->with('success', '更新しました');
}

2. DTO(データ保持+Getter)

final class TaskUpdateDto
{
    public function __construct(UpdateRequest $request)
    {
        $this->title = $request->input('title');
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}

3. Service(DTOからデータを取り出して保存)

public function handle(TaskUpdateDto $dto): void
{
    $task = new Task();
    $task->applyTaskAttributes($dto);
    $task->save();
}

4. Model(DTOを適用)

public function applyTaskAttributes(TaskUpdateDto $dto): void
{
    $this->title = $dto->getTitle();
}

📁 テンプレートでの構成ルール

提供する構造化テンプレートでは、DTOは以下のようにステータス単位で整理しています。

app/
├── DataTransferObjects/
│   └── Status/
│       └── Ongoing/
│           └── Update/
│               └── TaskUpdateDto.php
  • Status/ … 各ステータス単位(例:Ongoing, Completed など)
  • Update/ … 処理単位(Update, Register, Transition など)
  • TaskUpdateDto.php … 実際のDTOクラス

このように分けることで、

  • 関連する処理ごとにDTOを管理できる
  • 肥大化や流用による混乱を防げる
  • ドメイン知識に沿ったディレクトリ構成が保てる

といった利点があります。

補足:DTOにセッターを基本用意しない理由

DTOを使う際、セッター(setXxx())を使って後から値を変更する構成は、提供するテンプレートでは原則として採用していません

理由は以下の通りです。


❌ セッターがあると「どこで値が変わったか分からない」

DTOは「バリデーション済みの確定データを保持する役割」を持ちます。
セッターを用意すると、処理の途中で値が変更される可能性が生まれ、値の一貫性が崩れるリスクが高まります。

$dto = new TaskUpdateDto($request);
$dto->setTitle('別の値'); // ← どこで変えた?なぜ変えた?

✅ コンストラクタで受け取り、getterで公開するスタイル

final class TaskUpdateDto
{
    public function __construct(UpdateRequest $request)
    {
        $this->title = $request->input('title');
    }

    public function getTitle(): string
    {
        return $this->title;
    }
}

このように「受け取ったら値は固定」「公開はgetter経由のみ」とすることで、
データの流れが明確で、安心して使える構造になります。

例外対応

  • セッターのメソッド名は明確に(例:withResolvedUser()
  • Factoryパターンや専用ビルダーを使うのが理想

例外的にセッターを使うケースと注意点

どうしても後から値を差し込まなければならない場面(例:Session情報の付与や内部処理での補完)もあります。

その場合は、以下のような制限付きで実装するのが安全です:

  • セッターは「明示的な処理用」としてメソッド名を工夫する(例:withResolvedUser()
  • セッターを privateprotected にして、外部から直接変更できないようにする
  • セッターではなく「ビルダー形式」や「Factoryクラス」での生成に切り替える

DTOは“データを受け取ったらそのまま渡す”のが本来の思想です。
値の変更が必要になった時点で「別のクラスを用意すべきか?」と立ち止まって検討するのが理想です。

補足:DTOへ渡す前に値を加工するならどこでやるべきか?

「リクエストから受け取った値をそのまま使うのではなく、何らかの加工をしてからDTOに渡したい」
こういったケースも実務ではしばしば登場します。


🎯 原則:DTOに渡す時点で「加工済みの確定値」であるべき

DTOは「加工・整形する場所」ではなく、整った値を“安全に運ぶ”ための構造です。
そのため、加工処理はDTOの外側(前段階)で行うのが基本方針です。

✅ 加工のおすすめタイミング別パターン

タイミング 主な用途 加工例
FormRequest 入力データの整形(サニタイズ) trim、null変換、文字種統一など
Controller内(DTO生成直前) 業務ルールを伴う補完 例:空ならデフォルト施設IDを入れる
Factory/Builder データ変換が複雑な場合 例:複数項目をマージ/日時形式変換

🔍 具体例:trimやnull変換をしたい場合

public function __construct(UpdateRequest $request)
{
    $this->title = trim($request->input('title', ''));
}

これはDTO内部で書いても動作しますが、本来は FormRequestの時点で加工しておく方がベター です。

public function prepareForValidation(): void
{
    $this->merge([
        'title' => trim($this->input('title', '')),
    ]);
}

⛔️ DTOでやらない方が良い加工処理の例

  • $request->has('xxx') などの条件付き処理
  • strtoupper()Carbon::parse() など複雑な変換
  • 他のモデルや外部データに依存する補完

これらは、ControllerまたはFactory層で完了させてからDTOに渡すのが理想です。


構造化テンプレートにおけるDTOルールの意義

私が開発しているLaravel構造化テンプレートでは、DTOに関して以下のような明確なルールを定義しています。

これは、実務での混乱や属人化を防ぎ、誰が見ても同じルールで開発・保守ができるようにするための設計です。


✅ 統一されたDTOの設計ルール

ルール 内容
📦 1ファイル = 1DTO 処理単位ごとに独立したDTOを定義
📁 ステータス単位のディレクトリ構成 Status/Ongoing/Update/TaskUpdateDto.php のように整理
🧪 値はコンストラクタで受け取る $request->input() はDTOの外で済ませるか、内部で限定的に処理
🔒 セッター禁止(原則) 値の変更を防ぐため、getterのみ公開
🧼 加工処理はDTOの外 前段で加工し、DTOは受け取り専用にする

🚀 DTOルールがもたらす効果

1. チーム内の実装ルールが揃う

  • 「DTOならこう書く」という統一感があり、属人性が排除されます。
  • 後から参加したメンバーもコード構造を把握しやすくなります。

2. 型安全と責務分離が徹底される

  • DTOは「型付きの値を運ぶ専用クラス」であり、責務が非常にクリアです。
  • ModelやServiceとの連携も明確になり、バグの温床が減ります。

3. 拡張時にも柔軟に対応できる

  • 項目追加や変更もDTO内に閉じることで影響範囲が明確に。
  • 他のDTOや処理に影響せず、局所的な修正が可能になります。

📌 現場で「ありがち」な罠を防ぐ

  • $request->all() に頼って値を受け取ってしまう
  • DTOにロジックや条件分岐が入り込む
  • データの意味や前提が不明確なまま処理される

こうした問題は、ルールが曖昧なまま開発を進めると必ず発生します。
構造化テンプレートでは、それを最初から「仕組み」で防ぐ」 設計になっています。


まとめ

本記事では、LaravelでDTO(Data Transfer Object) を導入する意義と、
構造化テンプレートにおける設計ルールについて詳しく解説しました。

🧠 本記事のポイント

  • DTOは「バリデーション済みの値を安全に渡すための専用クラス」
  • getterのみを使って値を公開し、セッターは原則使用しない
  • 加工処理はDTOの外で行い、DTOは“確定データ”のみを保持する
  • 構造化テンプレートでは、DTOの設計・配置・命名をルール化して運用

DTOを導入することで、Controller → Service → Model というデータの流れが明快になり、
保守性・再利用性・テスト性すべてにおいて設計が強化されます。


次回予告:Service層の役割と設計ルール

次回は、「Service層」に焦点を当てて解説します。

  • handleメソッドに統一する理由
  • トランザクション制御の位置づけ
  • Serviceの責務とRepositoryとの分離

など、Laravelを「単なる便利なフレームワーク」から、構造化された業務アプリ基盤として活用するための設計戦略をお届けします。


🚀 テンプレートで実践できます!

👉 Laravel構造化テンプレート(無料公開)

  • Laravel 11 / 12 対応
  • PHP 8.2+

🙌 フォロー・スキも励みになります!

今後もLaravelにおける設計・構造化ノウハウをZennで発信していきます。
記事が役に立った!と思っていただけたら、ぜひ「スキ」や「フォロー」をよろしくお願いします!

Discussion