🧩

LaravelでControllerを“1Action単位”に分離する理由

に公開

LaravelでControllerを“1Action単位”に分離する理由

はじめに

Laravelは習得しやすく、開発スピードも速いフレームワークです。しかし、機能追加やチーム開発が進むにつれて、以下のような課題に直面したことはありませんか?

  • Controllerが肥大化し、何をしているのか分かりにくくなる
  • バリデーション、保存処理、ログ出力などが1つのメソッドに詰め込まれる
  • 誰がどの責務を持っているのか分かりづらくなる

こうした状態が続くと、メンテナンス性や保守コストが急激に悪化します。

そこで本記事では、「Controllerは1アクション単位で分離する」という構造化アプローチを紹介します。
実務で蓄積してきたパターンをベースに、「なぜ分離するのか?」「どう分離すればいいのか?」を、テンプレート設計とあわせて解説します。

従来の書き方の問題点

Laravelを使い始めた頃や、小規模なプロジェクトでは以下のようなControllerの書き方がよく見られます。

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

    $task = Task::findOrFail($id);
    $task->title = $request->input('title');
    $task->description = $request->input('description');
    $task->save();

    Log::info('Task updated', ['task_id' => $task->id]);

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

問題点

  • 1つのメソッドに複数の責務(バリデーション/保存/ログ/レスポンス)
  • テストしづらい構造
  • 責務が増えるにつれ肥大化

なぜ分離するのか?

Controllerを分離することで得られる最大のメリットは、「責務の明確化」と「変更に強い構造」です。

1. Controllerはプレゼンテーション層に徹する

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

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

2. 各層が単一責務になる

+---------------------------+
| Controller                |
+---------------------------+
            ↓
+---------------------------+
| Service                   |
+---------------------------+
            ↓
+---------------------------+
| DTO (Data Transfer Object)|
+---------------------------+
            ↓
+---------------------------+
| Model / Repository        |
+---------------------------+

3. テストと保守がしやすくなる

構造化テンプレートの設計戦略

Controller層の方針:1 Controller = 1 Action

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

    return redirect()->route('tasks.index');
}

フォルダ構成例

app/
├── Http/Controllers/Admin/Ongoing/UpdateController.php
├── Services/Status/Ongoing/UpdateService.php
├── DataTransferObjects/Status/Ongoing/Update/TaskUpdateDto.php

実際の構成イメージ

処理フロー

[ Routing ]
    ↓
[ Controller ]
    ↓
[ Service ]
    ↓
[ DTO ]
    ↓
[ Model / Repository ]

コード構成例

Controller

public function __invoke(UpdateRequest $request, int $id)
{
    $dto = new TaskUpdateDto($request);
    $this->updateService->handle($dto, $id);
}

DTO

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

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

Service

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

まとめ

  • Controllerを1Actionごとに分離することで、見通し・保守性が大幅に向上する
  • DTO・Service・Modelを適切に役割分担することで再利用性とテスト性も高くなる
  • テンプレートではこの分離をルールとして標準化

次回予告

次回は、「DTOはなぜ必要なのか?」をテーマに、DTOの導入メリットや設計ポイントを詳しく解説します!

🚀 テンプレート紹介

本記事の構成は、無料で公開している「Laravel構造化テンプレート」にそのまま導入されています。
業務アプリの設計に悩んでいる方は、ぜひ参考にしてみてください!

👉 無料テンプレートはこちら(GitHub)

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

「いいね」や「フォロー」をいただけると励みになります!今後もLaravel構造化・実務ノウハウを発信していきます。

Discussion