【Laravel】Form Requestを活用してバリデーションを整理する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回はService・Repositoryパターンで、ControllerからDB操作や業務ロジックを追い出す方法を紹介しました。

今回は、Controllerに残りがちなもう一つの塊「バリデーション」を整理する、Form Requestという機能を扱います。

Controllerに書いたバリデーションの問題点

よくある書き方

バリデーションは、こんな風に $request->validate() でControllerに直接書くこともできます。

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|max:255',
        'body' => 'required|min:10',
        'category_id' => 'required|exists:categories,id',
    ]);

    // 保存処理...
}

短いアプリならこれでも困りませんが、更新用のメソッドでも似たルールが必要になると、同じバリデーションルールをコピーして回ることになります。

何が問題なのか

問題は主に3つあります。

  • ルールが増えるほどControllerのメソッドが長くなる
  • 同じリソースの storeupdate でルールがほぼ重複する
  • 「誰がこのリクエストを送っていいか」という認可のチェックが混ざりやすい

こうした「バリデーションルールの置き場所」と「認可のチェック」を専用のクラスに切り出す仕組みが、Form Requestです。

Form Requestの基本

作成コマンド

Form Requestは、Artisanコマンドで簡単に生成できます。

php artisan make:request StorePostRequest

実行すると、app/Http/Requests/StorePostRequest.php が作られます。

rules()とauthorize()

生成されたクラスには、主に2つのメソッドが用意されています。

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
            'body' => 'required|min:10',
            'category_id' => 'required|exists:categories,id',
        ];
    }
}

authorize() は「このリクエストを送る権限があるか」を、rules() は「バリデーションルール」を返します。

実装手順

基本的なバリデーションルール

投稿の作成と更新でルールを分けたい場合は、それぞれ専用のForm Requestを用意します。

// app/Http/Requests/UpdatePostRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        $post = $this->route('post');
        return $this->user()->id === $post->user_id;
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
            'body' => 'required|min:10',
            'category_id' => 'sometimes|exists:categories,id',
        ];
    }
}

更新の場合は「自分が投稿した記事だけ編集できる」という認可ルールを、authorize() にそのまま書けるのが便利なポイントです。

カスタムエラーメッセージ

デフォルトのエラーメッセージを日本語で分かりやすくしたい場合は、messages() メソッドを追加します。

public function messages(): array
{
    return [
        'title.required' => 'タイトルを入力してください。',
        'title.max' => 'タイトルは255文字以内で入力してください。',
        'body.required' => '本文を入力してください。',
        'body.min' => '本文は10文字以上で入力してください。',
    ];
}

Controllerでの利用

Controller側は、型ヒントに Request の代わりにForm Requestのクラス名を指定するだけです。

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Services\PostService;

class PostController extends Controller
{
    public function __construct(
        private PostService $postService
    ) {}

    public function store(StorePostRequest $request)
    {
        $post = $this->postService->createPost(
            $request->validated(),
            auth()->id()
        );

        return redirect()->route('posts.show', $post);
    }
}

Controllerのメソッドが呼ばれた時点で、バリデーションと認可のチェックはすでに完了しています。

失敗した場合は自動的に元のフォームへリダイレクトされ、エラーメッセージがセッションにセットされます。

つまずきポイント:authorize()をfalseのままにしてハマった話

かつコーチが実際につまずいた話

私が最初にForm Requestを使ったとき、しばらく原因不明の403エラーに悩まされたことがあります。

❌ Before:authorize()の中身を確認せず放置する

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return false; // 生成時のデフォルトのまま放置していた
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
        ];
    }
}

Artisanで生成した直後の authorize()false になっているバージョンがあり、それに気づかずルールだけ書いて動かしていました。

結果、フォームを送信するたびに403エラーが返ってきて、「ルールは合っているはずなのに、なぜ弾かれるのか」としばらく悩みました。

✅ After:authorize()の返り値を用途に応じて明示的に設定する

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // ログイン済みユーザーなら誰でも投稿可能
    }

    public function rules(): array
    {
        return [
            'title' => 'required|max:255',
        ];
    }
}

Form Requestを生成したら、rules() だけでなく authorize() の中身も必ず確認する、というのが私の中でのチェックリストに加わりました。

認可のロジックが不要な場合は true を返す、必要な場合は条件をきちんと書く、というのを意識するだけで防げるミスです。

応用:Service層と組み合わせる

バリデーション済みデータのみを渡す

Form Requestで検証を終えたデータは、$request->validated() で取得できます。

Service層に渡す際は、この validated() の結果だけを渡すようにすると、「未検証の生データがどこかに紛れ込む」という事故を防げます。

public function store(StorePostRequest $request)
{
    // $request->all() ではなく、必ず validated() を使う
    $post = $this->postService->createPost(
        $request->validated(),
        auth()->id()
    );

    return redirect()->route('posts.show', $post);
}

前回紹介したPostServiceのようなクラスと組み合わせると、Controllerは「認可・バリデーション済みのデータをServiceに渡すだけ」というシンプルな役割に完全に絞り込めます。

Form Requestは単体でも便利ですが、Service層と組み合わせることで、責務分担の効果が何倍にも高まります。

まとめ

この記事のポイント

  • Form Requestは、バリデーションルールと認可チェックをControllerから切り出す専用クラス
  • php artisan make:request で生成し、rules()authorize() を実装する
  • authorize() のデフォルト値をそのまま放置すると、意図しない403エラーにつながる
  • Service層と組み合わせ、validated() の結果だけを渡すことで安全性が高まる

次に読むべき記事

バリデーションとビジネスロジックの置き場所が整理できたら、次はプロジェクト全体のディレクトリ構成についても見直していきましょう。

→ 次の記事:Laravelのディレクトリ構成をカスタマイズする考え方

タイトルとURLをコピーしました