LaravelでWebアプリケーションやAPIを開発する際、フォーム送信やリクエストパラメータの妥当性を検証する「バリデーション」は欠かせない機能です。
しかし、多くの入門記事で紹介されているように Controller(コントローラー)のアクション内に直接 $request->validate([...]) を書き続けていると、コントローラーのコードが数十行・数百行と肥大化し、可読性やテスト容易性が著しく低下する 「Fat Controller(肥大化コントローラー)」 というアンチパターンに陥ってしまいます。
そこでLaravelが標準で提供している強力な設計パターンが 「FormRequest(フォームリクエスト)」 です。FormRequestを活用することで、バリデーションロジックやユーザーの権限認可(アクセス権チェック)を専用のリクエストクラスへ完全に分離し、単一責任の原則(SRP) に基づいたクリーンで保守性の高いアーキテクチャを実現できます。
- Controllerからバリデーションを完全分離するメリットとFat Controllerの解消法
php artisan make:requestコマンドによる生成と基本構造rules()によるルール定義とauthorize()による権限認可(Gate/Policy連携)- マスアサインメント脆弱性を防ぐ
$request->validated()のベストプラクティス - 【実務頻出】新規作成(POST)と更新(PUT/PATCH)でルールを柔軟に分岐させるテクニック
prepareForValidation()による入力データの事前サニタイズ(全角半角・トリミング)withValidator()による複数項目の相関チェックとpassedValidation()での事後加工- エラーメッセージ・属性名(attributes)のカスタマイズとトピッククラスター連携
LaravelのFormRequestとは?Controllerからバリデーションを分離するメリット
Fat Controllerの解消と単一責任の原則(SRP)
オブジェクト指向プログラミングにおける重要な設計原則の一つに 「単一責任の原則(Single Responsibility Principle: SRP)」 があります。これは「ひとつのクラスやメソッドは、ひとつの責務だけを担うべきである」という考え方です。
Webアプリケーションにおける Controller の本来の責務は、「HTTPリクエストを受け取り、適切なビジネスロジック(ServiceやModel)を呼び出して、HTTPレスポンス(ViewやJSON)を返す」 ことです。入力値の詳細な形式チェックやサニタイズ、権限認可といった責務までControllerが抱え込んでしまうと、コードが複雑怪奇になり変更に弱いシステムになってしまいます。
FormRequestを導入することで、Controllerとバリデーションの関係は以下のように劇的に改善されます。
| 比較項目 | Controller直接バリデーション($request->validate) | FormRequestクラスへの分離(推奨) |
|---|---|---|
| コードの配置 | Controllerのアクション内に直接記述 | app/Http/Requests/ 配下の専用クラス |
| Controllerの肥大化 | ✖ 肥大化する(ルールやメッセージで100行超) | ◯ スッキリ解消(数行の委譲コードのみ) |
| 再利用性 | ✖ 低い(他アクションで同じルールをコピペ) | ◯ 高い(複数のControllerやAPIで共通利用可能) |
| 権限認可の統合 | $this->authorize() を個別に呼ぶ必要あり |
authorize() メソッドで自動実行(403制御) |
| 事前加工・事後処理 | Controller内に泥臭いサニタイズ処理が散乱 | ライフサイクルフック(prepareForValidation等)にカプセル化 |
| 単体テスト容易性 | HTTPリクエスト全体を実行しないとテスト困難 | FormRequest単体でバリデーションルールのテストが可能 |
php artisan make:request による生成とクラスファイルの基本構造
FormRequestクラスの作成は、LaravelのArtisanコマンドを使用します。
php artisan make:request UserStoreRequest
このコマンドを実行すると、app/Http/Requests/UserStoreRequest.php が自動生成されます。生成されたファイルの基本コードは以下の通りです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
/**
* ユーザーがこのリクエストを行う権限を持っているかを判定
*/
public function authorize(): bool
{
return false;
}
/**
* リクエストに適用するバリデーションルールを取得
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
// バリデーションルールをここに定義
];
}
}
FormRequestクラスは Illuminate\Foundation\Http\FormRequest を継承しており、内部的には通常の Illuminate\Http\Request の全機能($request->input()、$request->has()、$request->file() など)をすべて備えています。
生成されたクラスには、主に以下の2つのメソッドがあらかじめ定義されています:
authorize(): リクエストを送信したユーザーがその操作を実行する権限を持っているかを判定(真偽値を返す)rules(): フィールドごとの検証ルール配列を返す
FormRequestの基本実装フロー(rulesとauthorize)
FormRequestをControllerで利用する一連の基本的な実装手順を詳しく見ていきましょう。
rules()メソッドでの検証ルール定義とControllerでの依存性注入(DI)
まずは rules() メソッド内に検証ルールを定義します。例えば会員登録フォームを想定してみましょう。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UserStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true; // 認可チェックを通過させる(詳細は後述)
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
'email' => ['required', 'string', 'email:rfc,dns', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'password_confirmation' => ['required', 'string', 'min:8'],
'terms' => ['accepted'],
];
}
}
Laravelでは
'required|string|max:50' のようなパイプ(|)区切り記法もサポートされていますが、実務では ['required', 'string', 'max:50'] のように配列形式で定義することを強く推奨します。後述するカスタムRuleオブジェクトや正規表現(Rule::unique、new Katakana())を組み合わせる際にパイプ記法だと構文エラーの原因になるためです。
作成したFormRequestをControllerで使用するには、アクションメソッドの引数で 型宣言(Type Hinting) を行います。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\UserStoreRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
class UserController extends Controller
{
/**
* 新規ユーザーを登録
*/
public function store(UserStoreRequest $request): RedirectResponse
{
// ここに到達した時点で、authorize() による認可と
// rules() によるバリデーションはすべて成功している!
$validated = $request->validated();
User::create($validated);
return redirect()->route('users.index')
->with('success', 'ユーザーを登録しました。');
}
}
Laravelのサービスコンテナが自動的に UserStoreRequest をインスタンス化し、Controllerのアクションメソッドが実行される前に自動的に認可とバリデーションを実行 します。
検証に失敗した場合は、Controller内のコードは一切実行されず、自動的に直前の入力画面へリダイレクト(エラーメッセージと直前の入力値 old() を伴う)されます。APIリクエスト(リクエストヘッダーに Accept: application/json が含まれる場合)では、自動的に HTTP ステータスコード 422 Unprocessable Content とJSON形式のエラーメッセージが返却されます。
authorize()メソッドの正しい使い方(Gate/Policyとの連携と403エラー対策)
php artisan make:request でクラスを生成した直後は、authorize() メソッドが return false; になっています。
もし return false; のままリクエストを送信すると、Laravelは直ちに 403 Forbidden(This action is unauthorized.) エラーをスローします。初心者がFormRequestを導入した際に最もつまずきやすいポイントがこの「403エラー」です。
// ⚠️ このままだと誰がアクセスしても 403 Forbidden になる!
public function authorize(): bool
{
return false;
}
① 全ユーザーに許可する場合(公開フォーム・新規登録など)
お問い合わせフォームや会員登録など、誰でもアクセス可能なエンドポイントの場合は明示的に return true; を返します。
public function authorize(): bool
{
return true;
}
② ログイン状態や特定権限をチェックする場合(Gate/Policy連携)
「管理者のみが実行できる操作」「自身の投稿のみ編集可能」といった認可ロジックは、authorize() メソッド内で判定します。FormRequest内では $this->user() で現在認証されているユーザーインスタンスを取得できます。
<?php
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
class PostUpdateRequest extends FormRequest
{
public function authorize(): bool
{
// ルートパラメータからPostモデルを取得(/posts/{post})
$post = $this->route('post');
// ログイン中のユーザーが投稿の作成者であるか、または管理者であるか判定
return $this->user() && ($this->user()->id === $post->user_id || $this->user()->is_admin);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:200'],
'body' => ['required', 'string'],
];
}
}
Policyクラスが定義されている場合は、$this->user()->can() や Gateファサードを呼び出して連携させるのが最も美しい設計です。認可の基本やポリシーの使い分けは Laravel GateとPolicyの違いと使い分け完全ガイド|認可(権限管理)の基本 もあわせてご参照ください。
use Illuminate\Support\Facades\Gate;
public function authorize(): bool
{
$post = $this->route('post');
// Policyの update メソッドを呼び出して認可判定
return Gate::allows('update', $post);
}
$request->validated() で安全な検証済みデータのみを取得するベストプラクティス
Controller内でユーザーの入力値を受け取る際、$request->all() や $request->input() を直接モデルの create() や update() に渡していませんか?これは重大なセキュリティホール(マスアサインメント脆弱性)の原因になります。
// ❌ 危険なアンチパターン:悪意あるリクエストで予期せぬカラムが上書きされる恐れがある
$user = User::create($request->all());
FormRequestを利用する最大のメリットのひとつが、$request->validated() メソッドです。
$request->validated() を実行すると、rules() メソッドで明示的に定義されたルールを通過した安全なデータのみ が連想配列として返されます。
// ⭕ 推奨されるベストプラクティス:検証を通過した安全なデータのみを取得
$validated = $request->validated();
User::create($validated);
例えば、攻撃者がリクエストに is_admin=1 や role=superadmin といった不正なパラメータを含めて送信してきたとしても、それらが rules() に定義されていなければ $request->validated() の返り値には一切含まれません。
さらに、Laravelでは $request->safe() メソッドを使用することで、検証済みデータから特定フィールドのみを抽出したり除外したりすることも可能です。
// 特定のフィールドのみを抽出
$userData = $request->safe()->only(['name', 'email']);
// パスワードなど特定のフィールドを除外して取得
$profileData = $request->safe()->except(['password', 'password_confirmation']);
実務で必須の高度なFormRequestテクニック
FormRequestの基本を押さえたら、実務開発の現場で日常的に使用する高度なライフサイクルフックと実践テクニックをマスターしましょう。
【実務頻出】新規作成(POST)と更新(PUT/PATCH)でバリデーションルールを分岐する
実務のCRUD機能では、新規作成と更新で求められるバリデーションルールが微妙に異なるケースが頻出します。
- 新規作成(POST): パスワードは必須、メールアドレスは一意(unique)
- 更新(PUT/PATCH): パスワードは空なら変更なし(nullable)、メールアドレスは「自分自身のIDを除外した一意性チェック」が必要
新規用と更新用で UserStoreRequest と UserUpdateRequest の2クラスに分ける設計も有効ですが、共通項目が多い場合は1つの UserRequest クラス内で HTTP メソッドやルートパラメータに応じて動的にルールを切り替える手法が非常にスマートです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
// ルートパラメータから対象のUserモデル(またはID)を取得(/users/{user})
$user = $this->route('user');
$userId = $user instanceof \App\Models\User ? $user->id : $user;
// 新規作成・更新で共通のルール
$rules = [
'name' => ['required', 'string', 'max:50'],
'email' => [
'required',
'string',
'email:rfc,dns',
'max:255',
// 更新時は自分自身のIDを除外して重複チェック
Rule::unique('users', 'email')->ignore($userId),
],
];
// HTTPメソッドに応じたルールの切り替え
if ($this->isMethod('post')) {
// 新規作成時:パスワード必須
$rules['password'] = ['required', 'string', 'min:8', 'confirmed'];
} elseif ($this->isMethod('put') || $this->isMethod('patch')) {
// 更新時:パスワードは入力された場合のみ検証(空なら変更なし)
$rules['password'] = ['nullable', 'string', 'min:8', 'confirmed'];
}
return $rules;
}
}
$this->isMethod('post') や $this->route('user') を活用することで、1つのFormRequestでDRY(Don’t Repeat Yourself)原則を保ちながらスマートにルールを制御できます。
prepareForValidation() による入力データの事前サニタイズ(全角半角変換・トリミング)
ユーザーからの入力データには、全角数字、前後の不要な空白、ハイフンの有無など、表記の揺れがつきものです。
バリデーションを実行する前にデータをクレンジング(サニタイズ)したい場合、FormRequestの prepareForValidation() メソッドをオーバーライドします。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class MemberRegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* バリデーション実行前に入力データを加工・整形する
*/
protected function prepareForValidation(): void
{
$this->merge([
// メールアドレスの前後の空白を除去し、小文字に統一
'email' => strtolower(trim((string) $this->input('email'))),
// 電話番号からハイフンを除去し、全角数字を半角数字に変換
'phone' => preg_replace('/[^\d]/', '', mb_convert_kana((string) $this->input('phone'), 'n')),
// 郵便番号の全角数字を半角に変換し、ハイフンを統一
'postal_code' => mb_convert_kana((string) $this->input('postal_code'), 'as'),
// フリガナの半角カナを全角カタカナに変換
'kana' => mb_convert_kana((string) $this->input('kana'), 'KV'),
]);
}
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255'],
'phone' => ['required', 'regex:/^0\d{9,10}$/'], // ハイフンなし10〜11桁
'postal_code' => ['required', 'regex:/^\d{3}-?\d{4}$/'],
'kana' => ['required', 'string'],
];
}
}
$this->merge([...]) を呼び出すことで、リクエスト内部のパラメータが上書きされます。これにより、サニタイズ済みの綺麗な値に対してバリデーションが実行 され、Controller側でも加工済みのクリーンなデータを $request->validated() で受け取ることができます。
withValidator() による複雑な相関チェック(条件付きカスタム検証)
「開始日は終了日より前の日付でなければならない」「会員種別が『法人』の場合は会社名の入力が必須」「キャンペーンコードが有効な期間であるかDBで照合する」といった、複数フィールドが絡み合う複雑な検証ロジックは、withValidator() メソッドを使って実装します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class EventBookingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'start_date' => ['required', 'date'],
'end_date' => ['required', 'date'],
'ticket_type' => ['required', 'in:single,group'],
'attendees' => ['required', 'integer', 'min:1'],
];
}
/**
* バリデータインスタンスの設定(アフターフックの追加)
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
// 1. 日付の相関チェック(開始日 <= 終了日)
if ($this->filled(['start_date', 'end_date'])) {
if ($this->date('start_date')->isAfter($this->date('end_date'))) {
$validator->errors()->add(
'end_date',
'終了日には開始日以降の日付を指定してください。'
);
}
}
// 2. 団体チケットの場合、参加人数が5名以上必要
if ($this->input('ticket_type') === 'group' && (int) $this->input('attendees') < 5) {
$validator->errors()->add(
'attendees',
'団体チケットをお申し込みの場合は、5名以上の参加人数を指定してください。'
);
}
});
}
}
$validator->after(function ($validator) { ... }) コールバックは、rules() の標準ルール検証がすべて終わった直後に呼び出されます。条件を満たさない場合は $validator->errors()->add('フィールド名', 'エラーメッセージ') で任意のエラーを追加できます。
passedValidation() を使った検証通過後のデータ加工
バリデーションがすべて無事に通過した後、Controllerに処理が移る直前でデータを変換・付加したいケースがあります。その際に役立つのが passedValidation() メソッドです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;
class PasswordChangeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'current_password' => ['required', 'current_password'],
'new_password' => ['required', 'string', 'min:8', 'confirmed'],
];
}
/**
* バリデーション成功後に実行される処理
*/
protected function passedValidation(): void
{
// 新しいパスワードをハッシュ化してリクエストに再格納
$this->replace([
'password' => Hash::make($this->input('new_password')),
]);
}
}
Controller側ではハッシュ化処理を記述する必要がなく、$request->only('password') や $request->input('password') で即座にDB保存を行えます。
エラーメッセージと項目名(attributes)のカスタマイズ
FormRequestでは、フォーム固有のエラーメッセージや項目名(プレースホルダー :attribute にバインドされる文字列)を簡単にオーバーライドできます。
messages()メソッドでの個別メッセージ上書き
特定のフォームでのみ、標準のエラー文言よりも親切・詳細なメッセージを出したい場合は、messages() メソッドを定義します。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class InquiryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' => ['required', 'email:rfc,dns'],
'content' => ['required', 'string', 'min:20', 'max:2000'],
'agree' => ['accepted'],
];
}
/**
* バリデーションエラーメッセージのカスタマイズ
*/
public function messages(): array
{
return [
'email.required' => '返信用のメールアドレスを入力してください。',
'email.email' => '正しいメールアドレスの形式(例: user@example.com)で入力してください。',
'content.min' => 'お問い合わせ内容は、状況を詳しく把握するため :min 文字以上でご入力をお願いいたします。',
'agree.accepted' => 'お問い合わせを送信するには、個人情報保護方針への同意が必要です。',
];
}
}
キー名は 'フィールド名.ルール名'(例: 'email.required')の形式で指定します。すべてのルールに対して一括で指定したい場合は 'フィールド名.*' や、ルール名単体('required')で指定することも可能です。
attributes()メソッドによる項目名の日本語化
Laravelのデフォルト状態では、エラーメッセージ内の項目名(:attribute)はフォームの input 名(例: content や email)がそのまま英語で出力されてしまいます。
FormRequest内の attributes() メソッドで日本語名を定義することで、自然な日本語エラーメッセージに変換できます。
/**
* バリデーション属性(項目名)の日本語化
*/
public function attributes(): array
{
return [
'email' => 'メールアドレス',
'content' => 'お問い合わせ内容',
'agree' => '利用規約への同意',
];
}
これにより、例えば 'content.min' の :attribute は :min 文字以上で入力してください。 というメッセージは 「お問い合わせ内容 は 20 文字以上で入力してください。」 と美しく置換されます。
アプリ全体のエラーメッセージを共通で日本語化・属性設定する方法は Laravel バリデーション日本語化完全ガイド|lang/ja/validation.php導入とattributes設定 で詳しく解説しています。
トピッククラスター連携(カスタムRule・日本語化・検索クエリ)
FormRequestは、Laravelの他のバリデーション・リクエスト機能と組み合わせることで真価を発揮します。
自作バリデーションRuleオブジェクトをFormRequest内で呼び出す手順
標準のバリデーションルール(required, numeric, email 等)だけではカバーできない業務固有の要件(例:全角カタカナのみ許可、日本の郵便番号形式、パスワードの強度チェック、外部APIとの照合など)がある場合、自作の Ruleオブジェクト を作成してFormRequestから呼び出します。
独自の複雑なビジネスロジック検証を行う場合は、Laravel カスタムバリデーションルールの作成手順とRuleオブジェクトの使い方完全ガイド を参照して自作Ruleを呼び出してください。
自作Ruleクラス(例: App\Rules\Katakana や App\Rules\PostalCode)をFormRequestの rules() 内に組み込むコードは以下のようになります。
<?php
namespace App\Http\Requests;
use App\Rules\Katakana;
use App\Rules\PostalCode;
use Illuminate\Foundation\Http\FormRequest;
class CustomerRegistrationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50'],
// 自作Ruleオブジェクト「Katakana」を適用
'name_kana' => ['required', 'string', 'max:50', new Katakana()],
// 自作Ruleオブジェクト「PostalCode」をハイフン必須で適用
'postal_code' => ['required', new PostalCode(requireHyphen: true)],
'address' => ['required', 'string', 'max:255'],
];
}
public function attributes(): array
{
return [
'name_kana' => 'お名前(フリガナ)',
'postal_code' => '郵便番号',
'address' => 'ご住所',
];
}
}
自作Ruleを組み合わせることで、FormRequestのコード行数をスッキリ保ったまま、高度な検証ロジックをプロジェクト全体で再利用できます。
複数条件の検索・絞り込みフォームにおけるFormRequestの活用
FormRequestは新規登録や更新フォームだけでなく、「複数条件の絞り込み検索機能」 でも絶大な効果を発揮します。
GETリクエストで送信される検索パラメータ(キーワード、カテゴリ、日付範囲、価格帯など)をFormRequestで事前にバリデーション・サニタイズすることで、不正な型やSQLインジェクションのリスクを排除した安全なクエリ構築が可能になります。
FormRequestで安全に検証した入力値を使って動的検索を行う実装は Laravel 複数条件の絞り込み検索機能 実装ガイド|whenメソッド・動的WHERE句 をご覧ください。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductSearchRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'keyword' => ['nullable', 'string', 'max:100'],
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
'min_price' => ['nullable', 'integer', 'min:0'],
'max_price' => ['nullable', 'integer', 'gte:min_price'],
'sort' => ['nullable', 'in:price_asc,price_desc,newest'],
];
}
}
Controllerでは $request->validated() で検証済みの安全な配列を受け取り、Eloquentの when() メソッドに渡すだけで、堅牢でエレガントな検索APIが完成します。
まとめ:保守性の高いFormRequest設計のチェックリスト
FormRequestは、Laravel開発においてFat Controllerを防ぎ、単一責任の原則を体現するための最重要ツールです。
最後に、実務でFormRequestを設計・実装する際のチェックリストを確認しましょう。
- [ ]
php artisan make:requestコマンドで生成し、単一目的のクラスとして分離しているか? - [ ]
authorize()メソッドのreturn false;を放置せず、適切な認可(Gate/Policyまたはtrue)を設定しているか? - [ ] Controller側では
$request->all()ではなく$request->validated()で安全な検証済みデータのみを取得しているか? - [ ] ルール定義はパイプ区切りではなく
['required', 'string']のような配列形式を使用しているか? - [ ] 全角半角変換や空白トリミングなどのサニタイズ処理は
prepareForValidation()にカプセル化しているか? - [ ] 複数フィールドにまたがる相関チェックは
withValidator()のアフターフックで実装しているか? - [ ] 独自性の高い複雑なビジネスロジックは自作Ruleオブジェクトを作成して呼び出しているか?
- [ ] 新規(POST)と更新(PUT/PATCH)でルールが異なる場合、HTTPメソッドやルートパラメータによる分岐を活用しているか?
適切なFormRequest設計を導入して、コントローラーをすっきりと保ち、保守性・堅牢性に優れたLaravelアプリケーションを構築しましょう!

コメント