【Laravel】Form Requestによるバリデーション設計|authorize・rules・カスタムエラーメッセージ

Laravel のコントローラに $request->validate([...]) を直接書くと、ルールが増えるほどメソッドが長くなります。権限チェックと入力検証も混ざりやすくなります。Form Request は、認可(authorize)とバリデーション(rules)を専用クラスに分離する仕組みで、コントローラは型ヒントするだけで検証済みデータを受け取れます。

細かな API は Laravel のメジャーバージョンで変わることがあります。手元の composer.jsonlaravel/framework に合わせ、Form Request Validation の該当バージョンを照合すると、再現しやすくなります。

はじめに

「更新 API では本人だけ許可したい」「エラーメッセージを日本語化したい」などの要件は、コントローラ内バリデーションだけでは整理しづらくなりがちです。Policy(モデルごとの操作可否を定義する認可機構)や Gate を別の場所に書くと、いつ・どの順番で実行されるかも追いにくくなります。

Form Request では、authorize で操作可否を、rules で入力ルールを定義します。コントローラメソッドの引数に Form Request を型ヒントすると、コントローラ本体が呼ばれる前に認可とバリデーションが走ります。

この記事では次の範囲を整理します。

  • Form Request の生成とコントローラへの組み込み
  • authorize と Policy の使い分け
  • rules の設計、messages / attributes によるエラー表示の調整
  • よくある失敗例と次に試せること

背景:Form Request が実行されるタイミング

Form Request は通常の HTTP リクエストを拡張したクラスです。ルート解決のあと、コントローラメソッドが呼ばれる前に次の流れになります(Form Request Validation)。

  1. authorize()false なら 403 を返し、コントローラは実行されない
  2. authorize() が通れば rules() に基づきバリデーション
  3. 失敗時は 422 または前画面へリダイレクト。成功時だけコントローラが実行される

3 番目の挙動は、リクエストが JSON を期待するかどうかで変わります。API クライアント向けでは 422 が返りやすく、従来の Web フォームでは back() 付きリダイレクトになりやすいです(Form Request Validation)。

メソッド役割
authorize()操作権限の有無(Policy / Gate など)
rules()入力値の検証ルール
messages()ルールごとのエラーメッセージ(任意)
attributes()項目名の表示名(任意)

実装と検証

Form Request の生成

Artisan でクラスを生成します。

php artisan make:request UpdatePostRequest

app/Http/Requests に配置されます。スケルトンには authorizerules が含まれます。

ルートとコントローラへの組み込み

ルートの {post} が、Form Request 内の $this->route('post') のキーになります。

Route::put('/posts/{post}', [PostController::class, 'update'])->middleware('auth');

コントローラ側では型ヒントするだけです。validate() を書く必要はありません。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\UpdatePostRequest;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;

class PostController extends Controller
{
    public function update(UpdatePostRequest $request, Post $post): RedirectResponse
    {
        $post->update($request->validated());

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

検証済みデータは $request->validated() で配列として取得できます。$request->safe() は検証済みデータをオブジェクトとして扱う API です(Form Request Validation)。

authorize と Policy

Policy はモデルごとの操作可否を定義するクラスです。$this->user()?->can('update', $post) はその判定を呼び出します。Policy が未導入なら authorize 内で直接比較しても動きます。同じ判定の重複を避けるなら Policy へ寄せる構成が一般的です。

更新系では、ルートモデルバインディングと Policy を組み合わせる例が多いです。authorizefalse のときは 403 になります。ルートモデルバインディングを使っている場合は $this->route('post') の代わりに $this->post とも書けます。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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

        return $post && $this->user()?->can('update', $post);
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string'],
        ];
    }
}
仕組み向いている用途
Middleware(auth など)ログイン済みかどうかなど、横断的なチェック
Policyモデル単位の権限を複数箇所で再利用
Form Request の authorizeそのエンドポイント固有の入口チェック

前節の例は、Form Request から Policy を呼ぶ一般的な形です。権限は Middleware やルートの can ミドルウェアだけで担保し、Form Request は入力検証に専念させるチームもあります。その場合は authorizereturn true にします。チーム内で「入口のどこで止めるか」を揃えることが重要です。

rules の設計

rules() は配列を返します。ルールオブジェクト(Rule::unique() など)や、メソッド引数への DI も利用できます(Form Request Validation)。

Store と Update でルールを分ける例です。unique の有無だけが違うパターンです。

// StorePostRequest
public function rules(): array
{
    return [
        'title' => ['required', 'string', 'max:255', 'unique:posts'],
        'body' => ['required', 'string'],
    ];
}

// UpdatePostRequest(unique だけ ignore)
public function rules(): array
{
    return [
        'title' => ['required', 'string', 'max:255', Rule::unique('posts')->ignore($this->route('post'))],
        'body' => ['required', 'string'],
    ];
}

カスタムエラーメッセージ(messages / attributes

デフォルトメッセージのままで問題なければ、定義は省略して構いません。画面向けに文言を変えるときは messages()attributes() を使います(Customizing the Error Messages)。

public function messages(): array
{
    return [
        'title.required' => 'タイトルは必須です。',
        'body.required' => '本文を入力してください。',
    ];
}

public function attributes(): array
{
    return [
        'title' => 'タイトル',
        'body' => '本文',
    ];
}

attributes():attribute プレースホルダの置き換えに使われます。messages() では 'title.required' のように 属性名.ルール名 のキーで指定します。

動作確認(403 / 422)

API ルートで 403 と 422 を切り分けて確認する最小例です(認証トークンや URL は環境に合わせて読み替えてください)。

# 権限のないユーザーで更新 → 403
curl -X PUT <https://example.test/api/posts/1> \
  -H "Authorization: Bearer OTHER_USER_TOKEN" \
  -H "Accept: application/json" \
  -d '{"title":"test","body":"test"}'

# 必須項目を空にして更新 → 422
curl -X PUT <https://example.test/api/posts/1> \
  -H "Authorization: Bearer OWNER_TOKEN" \
  -H "Accept: application/json" \
  -d '{"title":"","body":""}'

403 と 422 ではレスポンス body の形も異なります。

// 403(authorize が false)
{ "message": "This action is unauthorized." }

// 422(rules 違反)
{ "message": "...", "errors": { "title": ["タイトルは必須です。"] } }

実装時の注意点

認可とバリデーションの順序

authorize() はバリデーションより先に評価されます。存在しない ID へのアクセスを 404 にしたい場合は、ルートモデルバインディングや authorize 内の取得ロジックを合わせて設計します。

401 と 403 の分担

未ログインは auth ミドルウェア側で 401 またはログイン画面へリダイレクトします。authorize は「ログイン済みだが権限不足」の 403 を担当する、と役割を分けることが多いです。

Fat Request の回避

検証・認可以外のロジックまで Form Request へ詰め込むと、再利用とテストが難しくなります。永続化はコントローラや、ユースケースごとに切り出したクラスへ置く構成が扱いやすいです。

バージョン差分

after() による追加検証や $redirectRoute など、付随 API はドキュメントの該当バージョンで名称を確認してください。

失敗例:authorizefalse なのに「バリデーションエラー」と混同した

更新 API で Policy により authorize()false になったケースで、フロントが次の分岐だけを実装していました。

if (response.status === 422) {
  showValidationErrors(response.data.errors);
}

実際のレスポンスは 403 のため、画面は無反応に見え、原因調査に時間がかかりました。403 用に「権限がありません」、422 用にフィールドエラーを分岐するとよいです。

学び:Form Request は HTTP 入力の「契約」をクラスに閉じ込める場所

UpdatePostRequest のようなクラス名は、ルートとセットで「この操作で受け付けるフィールドと権限」を表せます。コントローラからバリデーション配列が消えると、メソッドはユースケースの処理に集中しやすくなります。

まとめ

  • Form Request はコントローラ実行前に authorizerules の順で評価される。
  • authorizefalse のときは 403。rules 違反は 422 またはリダイレクト。権限不足を 422 の errors に載せない。
  • messages / attributes で表示文言を調整でき、Store / Update でクラスを分けるとルール管理がしやすい。

次に試せること

最短ルート

  1. make:request で Form Request を生成する
  2. コントローラの $request->validate() を型ヒントに置き換える
  3. 権限あり・なし、不正入力の 3 パターンで 403 / 422 を 1 回ずつ確認する

403 / 422 の突き合わせチェックリスト

  • 権限のないユーザーで更新 → 期待 403
  • 必須項目を空にして更新 → 期待 422
  • フロントのエラーハンドラが上記 2 つを区別している

Source

LaravelLaravel,PHP

Posted by 千原 耕司