メシのタネ

めしのたねになるIT情報配信サイト


Laravel Controllerの“万能感”、そこに幸せはあるのかい?


  1. Laravel
  2. Laravel Controllerの“万能感”、そこに幸せはあるのかい?

Laravelに触れたとき、「こんなに簡単なのか…!」と驚く瞬間ってありますよね。
たとえば、以下のようなControllerの一例を見てみましょう。

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required',
        'body'  => 'required',
    ]);

    Post::create($validated);

    return response()->json(['message' => '投稿完了']);
}

入力のバリデーション、DBへの保存、レスポンス返却――ほんの数行で一連の処理が完結してしまいます。初めて触れる人なら「これこそLaravelのすごさだ!」と感動するのも無理はありません。

しかし、この感動を頼りに進めていくと、次第にControllerのコード量はどんどん増えていきます。はじめはシンプルだったはずなのに、気づけば複雑な機能が集中する“重たいController”になってしまうケースも少なくありません。

この記事では、自分自身の「Laravel Controller体験記」をもとに、

  • なぜ最初にControllerに感動するのか
  • なぜ行き過ぎたControllerが生まれてしまうのか
  • どうやってControllerをうまく扱うべきか

について整理していきます。

次章では、改めてLaravelの「MVC」の「C」にあたるControllerがどのような立場・役割を担っているのかを見直してみましょう。

Controllerの役割ってそもそも何だっけ?

LaravelはMVC(Model-View-Controller)という設計思想に基づいて構築されたフレームワークです。
MVCの中でもControllerの役割は、「ユーザーから受け取ったリクエストに応じて、アプリケーションが何をすべきかを決めること」にあります。

🚦 ControllerはLaravelの“交通整理役”

ユーザーがフォームを送信したり、ページにアクセスしたりすると、必ず何らかの処理が必要になります。
その処理を「どこで・どうやって実行するか」を判断しているのがControllerです。

たとえば、以下のようなルーティングを定義すると:

Route::post('/posts', [PostController::class, 'store']);

この記述で、特定のURL /posts に対して、どのControllerのどのメソッドが呼ばれるのかを明示的に指定できます。
つまり、Controllerがアプリケーションの流れを整理し、処理を的確な場所へと導いてくれるわけです。

👷 Controllerは「司令塔」、でも現場作業員ではない

ただし、Controllerが「なんでもできるからといって、すべてを自分でやる存在」ではありません。
LaravelのControllerはあくまで処理の指揮官であって、自らがすべてのロジックを抱える必要はないのです。

Controllerは主に以下のような役割を担います:

  • 必要なバリデーションを呼び出す
  • データ操作はModelなど他のクラスに任せる
  • ユーザーに返すレスポンス(ViewやJSONなど)を決定する

つまり、「何をするか」を決めるのがControllerであり、実際の作業は別のクラスに任せるのが理想的な姿です。

🤝 Controllerに全部書きがちな理由(あるある体験)

ただし、Controllerが「なんでもできるからといって、すべてを自分でやる存在」ではありません。
LaravelのControllerはあくまで処理の指揮官であって、自らがすべてのロジックを抱える必要はないのです。

Controllerは主に以下のような役割を担います:

  • 必要なバリデーションを呼び出す
  • データ操作はModelなど他のクラスに任せる
  • ユーザーに返すレスポンス(ViewやJSONなど)を決定する

つまり、「何をするか」を決めるのがControllerであり、実際の作業は別のクラスに任せるのが理想的な姿です。

Controllerだけで「入力から出力まで」を書いてみる

Laravelに初めて触れたとき、「意外と簡単に動くんだな」と感じる瞬間があります。
フォームの入力を受け取り、データを保存して結果を返す、といった処理も、Controllerだけで完結できるからです。

ここでは、「フォームから送信されたデータを受け取り、データベースに保存して結果を返す」流れを、シンプルに整理していきましょう。

Step 1:まずは「Postモデル」を作成する

Laravelでデータを操作するときは、「Model」というクラスを使います。
今回は例として、「投稿(Post)」というデータを扱ってみます。
次のartisanコマンドでPostモデルを作成します。

php artisan make:model Post -m

このコマンドで、次の2つのファイルが生成されます。

  • app/Models/Post.php(投稿データを扱うModel)
  • database/migrations/xxxx_xx_xx_create_posts_table.php(テーブル作成用のマイグレーション)

Step 2:マイグレーションでテーブルの構造を定義する

マイグレーションファイルに、保存したい情報を以下のように記述します。

public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

ファイルを保存したら、以下のコマンドでテーブルを作成します。

php artisan migrate

Step 3:Modelに保存可能な項目を指定する

Laravelでは、安全にデータを保存するために、どのカラムを保存可能にするかをModelで明示的に指定します。
そのために、$fillable プロパティを設定します。

// app/Models/Post.php
class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'body'];
}

💡 補足:HasFactoryについて
use HasFactory; は、テストやダミーデータ作成時に便利な機能です。
今回の保存処理だけであれば特に意識する必要はありませんが、Laravelが推奨するのでそのまま記述しておきます。

Step 4:Controllerで一連の処理を行う(storeメソッド)

Controllerの役割は、リクエストを受け取り、バリデーションし、データを保存し、レスポンスを返すことです。
この一連の流れを、store()メソッドにまとめます。

public function store(Request $request)
{
    // 入力データのバリデーション
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body'  => 'required|string',
    ]);

    // データベースに保存
    $post = Post::create($validated);

    // 結果をJSON形式で返却
    return response()->json(['message' => '投稿が完了しました。']);
}

store()メソッドでは次の3つを順番に行っています。

  1. バリデーション
    入力内容のチェック($request->validate()
  2. 保存処理
    バリデーション済みデータの保存(Post::create()
  3. レスポンスの返却
    JSONで結果を返す(response()->json()

完成したController(コード全体)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
        ]);

        $post = Post::create($validated);

        return response()->json(['message' => '投稿が完了しました。']);
    }
}

このように、LaravelではControllerひとつで「入力→保存→レスポンス」の流れが完結します。
少ないコードで多くの処理が実現できることこそ、Laravelの魅力の一つです。

ただし――
このままControllerに何でも書いていくと、コードは少しずつ重く、複雑になっていきます。

次章では、「Controllerに全部書いてしまうと、どんな問題が起こるのか?」を見ていきましょう。

動かしたい場合

先ほどのコードを動かしたい場合は以下実行お願いします。

web.phpにルートを追加する

routes/web.php に、さきほど作成した store() メソッドを呼び出すルートを追加します。

use App\Http\Controllers\PostController;

Route::post('/posts', [PostController::class, 'store']);

curlで投稿してみよう

curl -X POST http://localhost/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "初めての投稿", "body": "これはテスト投稿です。"}'

無事成功すれば以下のようなレスポンスが返ります。

{
    "message": "投稿が完了しました。"
}

Controllerの動作が確認できました。

保存されたデータを確認しよう

投稿が本当に保存されているか、Laravelの対話ツール tinker を使って確認してみましょう。

php artisan tinker
>>> App\Models\Post::all();

保存された投稿が表示されれば、データベースにもきちんと反映されています。
Laravelは、Controllerひとつでここまで完結できる――それを実感できたら、もう一歩Laravel使いに近づいています。

環境構築まだの人はこちらから

次へ進む前に

ここまでで、Controllerの中に「バリデーション → 保存 → レスポンス」を書くことで、ひととおりのアプリ的な流れができるという体験ができました。

でも、全部Controllerに書いてもいいんだっけ?
このまま増えていく処理は、将来的にどうなるんだろう?

次章では、その疑問に向き合いながら、「Fat Controller」問題へと話を進めていきます。

『何でもController』が抱えるリスクと、脱却のステップ

LaravelのControllerは非常に優秀で、リクエストの受付、入力のバリデーション、データ保存、レスポンスの返却までをシンプルに行えます。
そのため、開発初期はControllerに処理を集中させることも珍しくありません。

しかし、開発を続けていくうちに、Controllerにどんどん処理が追加され、コードが徐々に膨らんでいきます。その結果、「一目で何をしているか把握しづらい」Controllerが出来上がってしまうのです。

🙃 よくある「詰め込みすぎController」の例

実際のプロジェクトでもよく見かける、典型的な例を見てみましょう。

public function store(Request $request)
{
    if (! $request->user()->can('create', Post::class)) {
        abort(403, 'Unauthorized');
    }

    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string',
        'send_email' => 'boolean',
    ]);

    try {
        $summary = Http::post('https://api.*****.com/*******', [
            'text' => $validated['body']
        ])->json()['summary'] ?? '';

        $post = Post::create([
            'title' => $validated['title'],
            'body' => $validated['body'],
            'summary' => $summary,
        ]);

        if (!empty($validated['send_email'])) {
            Mail::to($request->user())
                ->send(new \App\Mail\PostCreated($post));
        }

        dispatch(new \App\Jobs\LogPostCreationJob($post->id));

        if ($request->routeIs('posts.store.web')) {
            return redirect()->route('posts.index')->with('status', '投稿完了');
        }

        return response()->json(['message' => '投稿が完了しました。', 'id' => $post->id]);

    } catch (\Throwable $e) {
        Log::error('Post保存エラー', ['error' => $e->getMessage()]);
        return response()->json(['message' => 'エラーが発生しました'], 500);
    }
}

一見すると問題ないようにも見えるかも知れません。しかし、これは問題大有りです。

  • 処理が多すぎて一目で何をやっているのかわからない
  • 小さな仕様変更でも影響範囲が広くなりやすい
  • テストが書きにくく、再利用性も低い
  • 保守すること自体がストレスになる

👓 Controllerは「自分でやる」から「任せる」へ

このような問題は、Laravelが便利で柔軟だからこそ起こり得ます。
つまり、Laravelの利便性が裏目に出てしまった状態です。

もちろん、これはLaravel自体の問題ではありません。むしろ、「最初にこれだけの処理をControllerで書ける」こと自体がLaravelの強みです。ただし、そこから一歩進んで、「Controllerをもっと軽く、整理された存在にする」という考え方を導入すると、コードは自然と整理されていきます。

「全部できる」のがLaravelの強み。だけど、そこから一歩先へ進もう。

LaravelのControllerが最初から万能なのは、このフレームワークの魅力です。 初めて動くものを作れたときの感動や手軽さは、そのまま開発を加速してくれます。

しかし、プロジェクトが進み、処理が増えていくにつれて、「コードが複雑になりすぎて管理しきれない」という問題に直面するでしょう。

そんな時、以下のように責任を分割することも一つの選択肢かも知れません。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Http\Resources\PostResource;
use App\UseCases\CreatePostUseCase;
use Illuminate\Http\JsonResponse;

class PostController extends Controller
{
    public function store(StorePostRequest $request, CreatePostUseCase $useCase): JsonResponse
    {
        $post = $useCase->execute($request->validated(), $request->user());

        return new PostResource($post);
    }
}
  • validationはRequestクラスへ
  • メインロジックはUseCaseへ
  • Responseの生成はResoucrceへ

こうして役割を分けることで、Controllerは「何をするかを判断し、それぞれの処理を適切な場所へ任せる」存在になります。

LaravelのControllerは最初に頼れる存在でありながら、プロジェクトの成熟とともに自然に後ろへ下がり、コード全体の流れを整理する立場へと変化していきます。 その変化を受け入れることで、プロジェクト全体のコードが読みやすく、保守しやすくなっていくのです。

次に進むときのヒント

最後に紹介した責務の分割は、決して「正しい形」を押し付けるものではありません。

「Controllerに処理を詰め込みすぎて困ってきたな」 「これ、もうちょっと分けられないかな」

そう思ったタイミングで、少しずつ取り入れていけば十分です。

Laravelは自由度が高いぶん、設計も段階的に育てていけるフレームワークです。便利さと整理のバランスを取りながら、自分たちに合ったスタイルを探してみてください。

そして次の章では、その最初のステップとして「Requestクラスを使ってバリデーションをControllerの外に出す」方法を紹介していきます。

Controllerを少し軽くしてみる。その小さな一歩から、コードの見通しが変わっていくかもしれません。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.

若い頃、「仕事中にハマったこと」や「誰かに共有したい技術的な気づき」をアウトプットしたくてブログを始めましたが、勢い任せでよく分からない記事を大量生産し、あえなく飽きて終了。

改めて今、キャリア15年分の経験や知識が、これからITエンジニアを目指す方や、同じような課題で悩んでいる現役エンジニアの「メシのタネ」になるような記事を残したいと思っています。
※過去の記事は見ると精神が崩壊するため、そっとしておいてください。

🛠 経歴という名の珍道中:
文系Fラン → 広告営業 → Web営業 → 通信営業 → Web進行 → 出版 → Web媒体運用 → ソフトウェアハウス → SES → フリーランス

専門教育も受けず、転職歴も多数。履歴書はまるで時系列の事故記録のようですが、試行錯誤を重ね、なんとかエンジニアとして食べています。

このブログでは、そんな「履歴書クラッシャー型エンジニア」が送る、
名古屋一敷居の低い、実務に役立つ技術ブログを目指します。

Laravel
Laravel Collection入門: mapとeachの違い、ちゃんと説明できますか?New!!
Laravel
Eloquentのリレーション徹底解説New!!
Laravel
Eloquentは知ってる。でも“理解してる”って言えるのか?New!!
Laravel
FormRequestのポテンシャル、半分も出せてない説New!!
Laravel
Laravel Controllerの“万能感”、そこに幸せはあるのかい?New!!
API
RESTってつまり何?Webエンジニアが悩まないためのAPI設計入門