【Laravel】Form Requestによるバリデーション設計|authorize・rules・カスタムエラーメッセージ
Laravel のコントローラに $request->validate([...]) を直接書くと、ルールが増えるほどメソッドが長くなります。権限チェックと入力検証も混ざりやすくなります。Form Request は、認可(authorize)とバリデーション(rules)を専用クラスに分離する仕組みで、コントローラは型ヒントするだけで検証済みデータを受け取れます。
細かな API は Laravel のメジャーバージョンで変わることがあります。手元の composer.json の laravel/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)。
authorize()がfalseなら 403 を返し、コントローラは実行されないauthorize()が通ればrules()に基づきバリデーション- 失敗時は 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 に配置されます。スケルトンには authorize と rules が含まれます。
ルートとコントローラへの組み込み
ルートの {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 を組み合わせる例が多いです。authorize が false のときは 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 は入力検証に専念させるチームもあります。その場合は authorize を return 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 はドキュメントの該当バージョンで名称を確認してください。
失敗例:authorize で false なのに「バリデーションエラー」と混同した
更新 API で Policy により authorize() が false になったケースで、フロントが次の分岐だけを実装していました。
if (response.status === 422) {
showValidationErrors(response.data.errors);
}
実際のレスポンスは 403 のため、画面は無反応に見え、原因調査に時間がかかりました。403 用に「権限がありません」、422 用にフィールドエラーを分岐するとよいです。
学び:Form Request は HTTP 入力の「契約」をクラスに閉じ込める場所
UpdatePostRequest のようなクラス名は、ルートとセットで「この操作で受け付けるフィールドと権限」を表せます。コントローラからバリデーション配列が消えると、メソッドはユースケースの処理に集中しやすくなります。
まとめ
- Form Request はコントローラ実行前に
authorize→rulesの順で評価される。 authorizeがfalseのときは 403。rules違反は 422 またはリダイレクト。権限不足を 422 のerrorsに載せない。messages/attributesで表示文言を調整でき、Store / Update でクラスを分けるとルール管理がしやすい。
次に試せること
最短ルート
make:requestで Form Request を生成する- コントローラの
$request->validate()を型ヒントに置き換える - 権限あり・なし、不正入力の 3 パターンで 403 / 422 を 1 回ずつ確認する
403 / 422 の突き合わせチェックリスト
- 権限のないユーザーで更新 → 期待 403
- 必須項目を空にして更新 → 期待 422
- フロントのエラーハンドラが上記 2 つを区別している





