Laravelのフォームバリデーションは、入力値を拒否するだけの仕組みではありません。
サーバー側で受け入れる条件と、利用者が入力を直すための案内を一つの設計として扱うことで、安全性と使いやすさを両立できます。
この記事では、登録フォームを例に、FormRequestでのルール管理、Bladeコンポーネント、エラー概要、フォーカス移動、非同期送信、多言語化、テストまでを順に整理します。
この記事で扱う実装
- FormRequestに検証、認可、入力の前処理を集約する
- 検証済みデータだけを保存処理へ渡す
- ラベル、補足、エラーをHTML上で関連付ける
- エラーの存在、場所、修正方法を文字で伝える
- 画面遷移と非同期送信の両方で通知とフォーカスを管理する
- キーボード操作と支援技術を含む確認手順を用意する
フォームバリデーションを三つの層で考える
フォームバリデーションとは、送信された値がアプリケーションの受け入れ条件を満たすか確認する処理です。
ブラウザ側の検証は早いフィードバックに役立ちますが、利用者や通信経路から届く値を最終的に判断するのはサーバー側です。
| 層 | 役割 | 実装の中心 |
|---|---|---|
| 入力前の案内 | 必須項目や形式を先に伝える | ラベル、補足文、選択肢 |
| 検証 | 受け入れ条件を判定する | FormRequest、HTMLの属性 |
| 修正支援 | 誤りの場所と直し方を伝える | エラー概要、項目別メッセージ、フォーカス |
三つの層を分けて考えると、「ルールは正しいが直し方が分からない」「画面には赤字が出るが読み上げでは気付けない」といった欠落を見つけやすくなります。
入力支援とエラー設計の全体像は、関連記事「フォームはなぜ使いにくくなるのか」でも確認できます。
FormRequestに検証ルールを集約する
FormRequestは、検証ルールと認可処理を専用クラスにまとめるLaravelの仕組みです。
コントローラーから入力条件を切り離せるため、ルール、文言、前処理の所在が明確になります。
リクエストクラスを作成する
php artisan make:request RegisterUserRequest
次の例では、登録フォームに必要な項目だけを定義します。
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterUserRequest extends FormRequest
{
public function authorize(): bool
{
// 認可を別の層で処理している登録画面の例
return true;
}
public function rules(): array
{
return [
'name' => ['bail', 'required', 'string', 'max:50'],
'email' => [
'bail',
'required',
'email',
'max:255',
Rule::unique(User::class, 'email'),
],
'password' => ['bail', 'required', 'string', 'min:8', 'confirmed'],
'agree' => ['accepted'],
'phone' => ['nullable', 'string', 'max:20'],
];
}
public function attributes(): array
{
return [
'name' => 'お名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
'agree' => '利用規約',
'phone' => '電話番号',
];
}
public function messages(): array
{
return [
'email.unique' => ':attributeは使用できません。',
'password.confirmed' => ':attributeと確認欄が一致しません。',
'agree.accepted' => ':attributeへの同意が必要です。',
];
}
protected function prepareForValidation(): void
{
$name = $this->input('name');
$email = $this->input('email');
$this->merge([
'name' => is_string($name) ? trim($name) : $name,
'email' => is_string($email) ? trim($email) : $email,
]);
}
}
bail:同じ項目で最初のルールに失敗したら、残りのルール評価を止めます。attributes():エラー文中の内部名を、利用者が理解できる項目名へ置き換えます。messages():既定文では意図が伝わらない箇所だけを上書きします。prepareForValidation():検証前の空白除去など、判定に必要な整形を行います。nullable:値が空なら後続ルールを適用しない任意項目を表します。
パスワードの長さや文字種はサービスの方針に合わせて決め、画面の案内とルールを一致させます。
たとえば画面に「英数字を含む」と表示するなら、長さだけでなく、その条件もサーバー側のルールで検証する必要があります。
保存には検証済みデータを使う
FormRequestはコントローラーが動く前に検証されますが、リクエストオブジェクトから未検証の入力が消えるわけではありません。
保存処理にはvalidated()またはsafe()から取り出した値を使います。
use App\Http\Requests\RegisterUserRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Hash;
public function store(RegisterUserRequest $request): RedirectResponse
{
$data = $request->safe()->only([
'name',
'email',
'password',
'phone',
]);
$data['password'] = Hash::make($data['password']);
$user = User::create($data);
auth()->login($user);
return redirect()
->route('dashboard')
->with('status', '登録が完了しました。');
}
この例は、モデル側の代入許可設定が済んでいることを前提にしています。
safe()->only()で保存対象を明示すると、フォームに項目が追加されたときも、保存する値をコードレビューで確認できます。
Bladeコンポーネントでラベルとエラーを関連付ける
同じ入力部品を画面ごとに手書きすると、id、ラベル、補足、エラーの関連付けが崩れやすくなります。
次のコンポーネントは、補足文とエラー文のIDをaria-describedbyへ渡し、エラー時だけaria-invalid="true"を設定します。
{{-- resources/views/components/form/input.blade.php --}}
@props([
'id',
'label',
'type' => 'text',
'name' => null,
'help' => null,
'required' => false,
'autocomplete' => null,
])
@php
$name = $name ?? $id;
$error = $errors->first($name);
$describedBy = collect([
$help ? $id . '-help' : null,
$error ? $id . '-error' : null,
])->filter()->implode(' ');
@endphp
<div class="form-field">
<label for="{{ $id }}">
{{ $label }}
@if($required)
<span>(必須)</span>
@endif
</label>
<input
id="{{ $id }}"
name="{{ $name }}"
type="{{ $type }}"
value="{{ $type === 'password' ? '' : old($name) }}"
@if($required) required @endif
@if($autocomplete) autocomplete="{{ $autocomplete }}" @endif
@if($describedBy !== '') aria-describedby="{{ $describedBy }}" @endif
@if($error) aria-invalid="true" @endif
class="form-input {{ $error ? 'is-invalid' : '' }}"
>
@if($help)
<p id="{{ $id }}-help">{{ $help }}</p>
@endif
@if($error)
<p id="{{ $id }}-error">{{ $error }}</p>
@endif
</div>
必須であることは、色や記号だけに頼らず「必須」という文字でも示します。
ネイティブHTMLのrequiredを使う場合、同じ意味のaria-requiredを重ねる必要はありません。
赤い枠は視覚的な補助として使えますが、エラーの内容は必ず文字で提示します。
入力部品を呼び出す
<x-form.input
id="name"
label="お名前"
required
autocomplete="name"
/>
<x-form.input
id="email"
type="email"
label="メールアドレス"
help="登録後の連絡を受け取れるアドレスを入力してください。"
required
autocomplete="email"
/>
<x-form.input
id="password"
type="password"
label="パスワード"
help="8文字以上で入力してください。"
required
autocomplete="new-password"
/>
autocompleteは入力の目的に合う値を選びます。
入力条件はプレースホルダーだけに置かず、入力中も読める補足文として残します。
エラー概要とフォーカスで修正箇所へ案内する
項目の直下にエラーがあっても、長いフォームでは送信に失敗したこと自体へ気付けない場合があります。
フォームの先頭にエラー概要を置き、各メッセージから該当項目へ移動できるようにします。
@if($errors->any())
<section
id="form-errors"
data-error-summary
tabindex="-1"
role="alert"
aria-labelledby="form-errors-title"
>
<h2 id="form-errors-title">
入力内容を確認してください
</h2>
<ul>
@foreach($errors->keys() as $field)
<li>
<a href="#{{ $field }}">
{{ $errors->first($field) }}
</a>
</li>
@endforeach
</ul>
</section>
@endif
この単純な例では入力名とHTMLのIDを同じにしています。
配列形式やドット記法の項目名を使う場合は、エラーキーとHTMLのIDを対応付ける変換を用意します。
画面遷移後は、概要を表示するだけでなく、キーボードの現在位置も移します。
document.addEventListener('DOMContentLoaded', () => {
document
.querySelector('[data-error-summary]')
?.focus();
});
tabindex="-1"は、概要を通常のTab移動には加えず、スクリプトからフォーカスできるようにする指定です。
項目ごとのエラーすべてを強いライブ通知にすると読み上げが重なるため、概要で全体を通知し、各項目のエラーはaria-describedbyで確認できる構成にします。
直し方が分かるエラーメッセージを書く
エラー文は「何が違うか」と「どう直すか」を、入力条件の範囲で具体的に書きます。
| 避けたい文 | 改善例 | 改善する理由 |
|---|---|---|
| 入力が不正です | メールアドレスを「name@example.com」の形式で入力してください。 | 対象と形式が分かる |
| 必須です | お名前を入力してください。 | 項目名が分かる |
| 短すぎます | パスワードを8文字以上で入力してください。 | 修正条件が分かる |
文字数や形式を案内するときは、実際のFormRequestのルールと一致させます。
システム内部のルール名やデータベース名を、そのまま利用者向けの文言へ出さないようにします。
多言語化では共通文と画面固有文を分ける
検証メッセージの共通文は言語ファイルへ置き、画面固有の文だけをFormRequestのmessages()で上書きします。
現在のLaravelで言語ファイルがない場合は、Artisanのlang:publishで公開してから、lang/ja/validation.phpなどを編集します。
// lang/ja/validation.php
return [
'required' => ':attributeを入力してください。',
'email' => ':attributeを正しい形式で入力してください。',
'attributes' => [
'name' => 'お名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
],
];
言語切替、URL、日付や通貨まで含む設計は「Laravelの多言語化とローカライゼーション」で整理しています。
非同期検証でも通知の順序を保つ
JavaScriptで送信するフォームでも、サーバー側のFormRequestを最終判定に使います。
入力内容が受け入れられなかった場合は、LaravelのJSONエラーレスポンスを受け取り、次の順で画面を更新します。
- 前回のエラー表示と
aria-invalidを取り除く - 項目別メッセージを表示し、入力と
aria-describedbyで関連付ける - エラー概要へ同じ内容をまとめる
- エラー概要を通知してフォーカスを移す
- 修正後の再送信でもサーバー側で再検証する
入力中に一文字ごと検証すると、完成前の値へ繰り返しエラーが出て負担になります。
形式確認はフォーカスが外れたとき、重複確認など通信が必要な判定は入力が落ち着いたときに行い、送信時にはすべてを再確認します。
role="alert"とaria-liveの使い分けは「ステータスメッセージの実装方法」も参考になります。
実務で迷いやすいフォームの扱い
| 場面 | 実装方針 | 確認点 |
|---|---|---|
| マルチステップ | 各段階の項目をサーバー側で検証し、完了済みの値を安全に保持する | 現在位置、戻る操作、段階をまたぐ整合性 |
| ファイルアップロード | ファイル種別と容量の条件を画面に示し、サーバー側でも検証する | 選択済みファイル名、失敗理由、再選択の方法 |
| 重複確認 | 入力中の確認は補助とし、送信時にRule::uniqueで再検証する |
利用できない場合の代替案、通信失敗時の扱い |
| チェックボックス群 | グループ名をfieldsetとlegendで示す |
グループ全体のエラーと個別選択肢の関係 |
| 日付や電話番号 | 受け入れる形式を入力前に示す | 表記例とサーバー側ルールの一致 |
どの場面でも、クライアント側の確認だけを通過条件にしません。
画面の補助機能が動かない場合でも、サーバー側が同じ条件で判定し、理解できるエラーを返す構成を保ちます。
公開前のアクセシビリティ確認
- すべての入力に、内容を説明するラベルがある
- 必須項目と入力形式を、入力前に文字で確認できる
- エラーが色だけでなく文字でも示される
- 補足文とエラー文が入力へプログラム上関連付けられている
- 送信失敗後にエラー概要へ移動できる
- エラー概要から各入力へ移動できる
- TabキーとShift+Tabキーで自然な順序に移動できる
- フォーカス位置が見た目で分かる
- 拡大表示でもメッセージが入力欄から離れない
- 成功時にも処理の完了が文字で伝わる
自動テストと手動確認を組み合わせる
FormRequestのルールはFeatureテストで、必須、形式、境界値、確認欄、任意項目を確認します。
HTMLについては、ラベルとID、aria-describedby、aria-invalid、エラー概要へのリンクを検査します。
自動テストだけでは、読み上げる順序やフォーカス移動の自然さまでは判断できません。
公開前にキーボードだけで入力と修正を行い、主要なブラウザーとスクリーンリーダーの組み合わせでも代表的なエラーを確認します。
入力条件と修正導線を一つの仕様にする
Laravel側のルール、画面上の案内、エラーメッセージが同じ条件を示しているかを最初に確認します。
そのうえで、エラーの存在、該当項目、修正方法を文字で伝え、キーボードと読み上げの利用者が迷わず修正箇所へ移動できるようにします。
FormRequestとBladeコンポーネントを共通化しておけば、新しいフォームでも同じ確認基準を適用しやすくなります。
この記事に関連する株式会社greedenの取り組み
フォームの使いやすさは、入力規則だけでなく、エラーの伝え方やフォーカス移動まで含めて検証してこそ高まります。株式会社greedenは、Webアクセシビリティチェックを通じ、利用者が迷いにくいフォーム改善の確認を支援しています。

