Laravelの入力チェックはどこまでFormRequestに置くべきか
Laravel バリデーション 設計で実務上の問題になりやすいのは、FormRequestに何でも書くか、逆にControllerへ条件分岐を残しすぎることです。
結論から言うと、FormRequestは入力形式の検証に寄せます。業務ルールはService層で扱います。そして、DB制約は最後の防衛線として設計します。
もちろん、Laravelのバリデーション機能は強力です。例えば、必須、文字数、形式、配列、unique、existsなど、多くの入力チェックを簡潔に書けます。
ただし、便利だからといって、すべての判断をFormRequestへ集めると保守しにくくなります。申込期限、在庫状態、権限、状態遷移のような判断は、入力形式ではなく業務ルールです。
この記事では、LaravelでAPIやフォームを実装する経験者向けに、FormRequest、Controller、Service層、DB制約の責務分担を整理します。
Laravel バリデーション設計の基本は責務を分けること
まず、バリデーションを「不正な値を弾く処理」とだけ見ると、設計判断が曖昧になります。そこで、入力形式、業務判断、永続化の安全性に分けて考えます。
| 置き場所 | 主な責務 | 例 | レビュー観点 |
|---|---|---|---|
| FormRequest | リクエスト入力の形式チェック | 必須、型、文字数、メール形式、配列構造 | DBや複雑な業務判断を持ちすぎていないか |
| Controller | リクエスト受け付けとレスポンス返却 | FormRequestを受け取り、Serviceを呼ぶ | if文で業務ルールを抱えていないか |
| Service層 | 業務ルールとユースケース制御 | 申込可能期間、状態遷移、権限、重複登録判断 | 入力形式チェックと混ざっていないか |
| DB制約 | データ整合性の最終防衛線 | NOT NULL、UNIQUE、外部キー、CHECK制約 | アプリだけに整合性を任せていないか |
この分け方にすると、レビュー時の会話が具体的になります。「このルールはFormRequestかServiceか」という議論ではなく、「入力形式なのか、業務判断なのか」を確認できます。
なお、Laravel公式のValidationドキュメントでは、FormRequest、カスタムメッセージ、validated input、配列バリデーションなどが整理されています。実装方法を確認するときは、まず公式の仕様に戻るのが安全です。
FormRequestに置く入力チェック
FormRequestに置きやすいのは、リクエスト単体で判断できる入力チェックです。つまり、DBの状態や現在の業務ステータスを深く見なくても判断できる条件です。
- 必須項目かどうか
- 文字数、数値範囲、日付形式が妥当か
- メールアドレスやURLの形式が正しいか
- 配列やネストした項目の構造が正しいか
- 列挙値として許可された値か
例えば、ユーザー登録APIなら、名前、メールアドレス、パスワードの形式はFormRequestで扱います。一方で、登録キャンペーン期間中かどうか、特定プランを選べるかどうかは業務ルールです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreMemberRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'max:255'],
'plan' => ['required', Rule::in(['standard', 'premium'])],
'started_at' => ['required', 'date'],
];
}
public function messages(): array
{
return [
'email.email' => 'メールアドレスの形式で入力してください。',
'plan.in' => '選択できないプランが指定されています。',
];
}
}
この例では、FormRequestが入力の形を整えています。Service層に渡る前に、最低限のデータ品質を保証できます。
ただし、ここに「現在の契約状態ではpremiumへ変更できない」といった判断を入れると、FormRequestが業務仕様を抱え始めます。その結果、同じルールをバッチ、管理画面、別APIで再利用しにくくなります。
Controllerはバリデーション結果を受け取るだけに近づける
次に、Controllerは薄く保ちます。LaravelではController内で`$request->validate()`を書くこともできますが、実務では処理が増えるほどControllerが読みにくくなります。
そのため、更新系APIや業務画面ではFormRequestを使い、Controllerは入力済みデータをService層へ渡す形に寄せます。これにより、リクエスト受付、業務処理、レスポンス返却の流れが見やすくなります。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreMemberRequest;
use App\Services\MemberRegistrationService;
use Illuminate\Http\JsonResponse;
class MemberController extends Controller
{
public function store(
StoreMemberRequest $request,
MemberRegistrationService $service
): JsonResponse {
$member = $service->register($request->validated());
return response()->json([
'id' => $member->id,
'name' => $member->name,
], 201);
}
}
このControllerなら、レビュー時に見るポイントが明確です。入力チェックはFormRequest、登録可否の判断はService層、レスポンス形式はControllerという切り分けになります。
また、Laravel公式のHTTP Requestsドキュメントも、リクエストから入力を取得する方法を確認する際に役立ちます。FormRequestで検証済みデータを扱う場合も、入力の扱い方をチームでそろえておくと安全です。
業務ルールはService層で明示する
一方で、業務ルールはService層に置く方が自然です。理由は、業務ルールはリクエストの形式ではなく、現在の状態や操作の意味に依存するからです。
例えば、次のような条件はFormRequestに押し込まない方が保守しやすくなります。
- 申込期間が終了しているか
- 現在のステータスから更新できるか
- ログインユーザーが対象データを操作できるか
- 同じ契約で重複申込にならないか
- 在庫、残高、利用上限を超えていないか
<?php
namespace App\Services;
use App\Exceptions\BusinessRuleException;
use App\Models\Member;
use App\Models\Plan;
class MemberRegistrationService
{
public function register(array $input): Member
{
$plan = Plan::where('code', $input['plan'])->firstOrFail();
if (! $plan->isAcceptingApplications()) {
throw new BusinessRuleException('このプランは現在申し込みできません。');
}
if (Member::where('email', $input['email'])->exists()) {
throw new BusinessRuleException('このメールアドレスは既に登録されています。');
}
return Member::create([
'name' => $input['name'],
'email' => $input['email'],
'plan_id' => $plan->id,
'started_at' => $input['started_at'],
]);
}
}
このように分けると、業務ルールに名前を付けやすくなります。さらに、同じ登録処理をAPI以外から呼ぶ場合でも、ルールを再利用しやすくなります。
ただし、Service層が巨大化する場合もあります。その場合は、業務ルールをDomain Service、Policy、Specification、専用の判定クラスへ分けることを検討します。
DB制約はLaravel バリデーション設計の最後の防衛線
Laravelのバリデーションがあるからといって、DB制約を省略するのは危険です。アプリケーション側のチェックは、同時実行、別経路の登録、将来の改修で抜ける可能性があります。
特に、NOT NULL、UNIQUE、外部キーは、アプリケーションの都合ではなくデータ整合性の要件として見るべきです。つまり、FormRequestやService層で確認していても、DBでも守る価値があります。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('members', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('email')->unique();
$table->foreignId('plan_id')->constrained();
$table->date('started_at');
$table->timestamps();
});
}
};
なお、Laravel公式のMigrationsドキュメントでは、カラム定義、インデックス、外部キー制約などが説明されています。DB制約は、バリデーションの代替ではなく、整合性を守る別の層として設計します。
一方で、DB制約だけではユーザーにわかりやすいエラーメッセージを返せません。そのため、画面やAPIではFormRequestとService層で先に意味のあるエラーを返し、DB制約で最終的に守る形が現実的です。
エラーメッセージは入力エラーと業務エラーで分ける
エラーメッセージも、責務を分けて設計します。入力エラーと業務エラーを同じ形式で返すと、フロントエンド側で扱いにくくなることがあります。
| エラー種別 | 例 | 返し方の考え方 |
|---|---|---|
| 入力エラー | emailが不正、nameが未入力 | field単位でerrorsに返す |
| 業務エラー | 申込期間外、状態変更不可 | 業務コードと表示メッセージを返す |
| 一意制約違反 | 同じメールが同時登録された | Serviceで検出しつつ、DB例外も考慮する |
| システムエラー | DB障害、想定外例外 | 詳細を返さずログと監視で追う |
例えば、APIではFormRequestのバリデーション失敗を422として返すことが多いです。一方で、「この注文は既に確定済みです」という業務エラーは、field単位の入力エラーではありません。
そのため、フロントエンドとAPIの間でエラー形式を決めておく必要があります。入力エラーはフォーム項目へ表示します。業務エラーは画面全体の通知や確認ダイアログへつなげます。
実務レビューで見たいLaravelバリデーション設計の観点
レビューでは、ルールが正しいかだけでなく、置き場所が適切かを確認します。特に、変更が入ったときにどこを直せばよいかが重要です。
- FormRequestに業務ルールが入りすぎていないか
- Controllerが条件分岐で肥大化していないか
- Service層で状態遷移や権限の判断が明示されているか
- DB制約がデータ整合性の最後の防衛線になっているか
- 入力エラーと業務エラーのレスポンス形式が混ざっていないか
- エラーメッセージがユーザーの次の操作につながるか
- テストでFormRequestとService層のルールを分けて確認できるか
また、入力チェックの追加が多い画面では、FormRequestの`rules()`だけでなく、テストの読みやすさも見ます。ルールが増えたときに、何を守りたいのかがテスト名から分かると保守しやすくなります。
さらに、uniqueやexistsを使う場合は、DBアクセスが増えます。大量項目や配列入力では、クエリ数やパフォーマンスも確認対象になります。
よくある質問
FormRequestにDB参照を書くのは避けるべきですか?
すべて避ける必要はありません。Laravelのuniqueやexistsのように、入力値が既存データと対応するかを見るルールは実務で使われます。
ただし、状態遷移、申込可否、権限、在庫のように業務判断を含む場合はService層へ寄せます。FormRequestは入力形式の検証を中心にする方が、責務が読みやすくなります。
Controllerでvalidateしても問題ありませんか?
小さい処理なら問題ない場合もあります。例えば、単純な検索フォームや一時的な管理機能ではController内のvalidateでも十分です。
一方で、更新系APIや業務ルールが増える画面ではFormRequestへ分けます。Controllerに入力チェック、業務判断、レスポンス整形が集まると、変更時の影響範囲が見えにくくなります。
DB制約があればFormRequestは不要ですか?
不要ではありません。DB制約はデータ整合性を守る最後の仕組みです。一方で、FormRequestはユーザーやAPIクライアントへ早く分かりやすくエラーを返すために使います。
つまり、FormRequest、Service層、DB制約は競合しません。役割が違うため、重ねて設計する方が安全です。
まとめ:Laravel バリデーション設計は入力形式と業務判断を分ける
Laravel バリデーション設計では、FormRequestを使うかどうかだけで判断しないことが重要です。まず、入力形式のチェックと業務ルールを分けます。
次に、Controllerを薄くし、Service層で業務判断を明示します。さらに、DB制約でデータ整合性を守ります。
この分け方にすると、レビューしやすくなります。また、API、フォーム、バッチ、管理画面など、複数の入口が増えても同じ業務ルールを扱いやすくなります。
Laravel/API設計の案件では、バリデーションの置き場所が保守性に直結します。FormRequest、Service層、DB制約の責務分担を意識してきた経験は、業務系WebシステムやAPI開発でも活かしやすいです。
一度カジュアル面談をしませんか?
株式会社bluenaは「高還元」と「伴走支援」を両立したSES企業です。単価の81〜86%を還元する報酬体系と、専任サポーターによる隔週1on1で、エンジニアが納得できるキャリアを実現します。
まとまっていなくてもOK——まずは現在地を聞かせてください。
カジュアル面談ですので、お気軽にお聞かせください。





