対象バージョン: Laravel 13.x / PHP 8.3 以上
フォームから送られてきた値は、そのまま保存せずに検証(バリデーション)する必要があります。Laravel にはバリデーションの仕組みが組み込まれており、ルールを書くだけで入力値をチェックできます。通常のフォーム送信で検証に失敗すると、エラーメッセージと入力値を持たせて元のフォームへ自動で戻すところまで Laravel が処理します。
この記事では、コントローラでの検証、Blade でのエラー表示、フォームリクエストへの切り出し、エラーメッセージの日本語化までを順に説明します。
前提
- Laravel 13.x(PHP 8.3 以上が必要です)
- 投稿(タイトルと本文)を作成するフォームを例にします
- ルートは次の 2 つを用意します
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/posts/create', [PostController::class, 'create']);
Route::post('/posts', [PostController::class, 'store']);
GET /posts/create でフォームを表示し、POST /posts で送信内容を受け取ります。
バリデーションルールの設定
コントローラで validate を使う
もっとも手軽なのは、コントローラのメソッド内で $request->validate() を呼ぶ方法です。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class PostController extends Controller
{
public function create(): View
{
return view('posts.create');
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
]);
// $validated を使って投稿を保存します
return redirect('/posts/create')->with('status', '投稿を保存しました。');
}
}
この例では、title は必須で 255 文字以内の文字列、body は必須の文字列であることを求めています。文字列に対する max:255 は文字数で判定するため、全角文字も 1 文字として数えます。
検証に通ると処理はそのまま続き、validate() の戻り値にはルールを指定したキーの値だけが入ります。ルールに書いていない項目が送られてきても $validated には含まれません。
成功時も必ずレスポンスを返してください。上の例ではフォーム画面へリダイレクトし、with() を使って、次の画面で一度だけ表示する完了メッセージをセッションに保存しています。
ルールは配列で書く
Laravel 13.x の公式ドキュメントでは、ルールを ['required', 'string', 'max:255'] のような配列で書いています。この記事のコードも配列形式で統一します。
'required|max:255' のようなパイプ区切りの文字列も 13.x で引き続き動作し、非推奨にもなっていません。ただし、regex ルールのパターンに | を含む場合、パイプ区切りの文字列では正しく解釈されずエラーになります。その場合は配列形式で書いてください。
検証に失敗したときの動き
検証に失敗すると Illuminate\Validation\ValidationException が投げられ、Laravel がリクエストの種類に応じたレスポンスを自動で返します。コントローラ側でエラー処理を書く必要はありません。
通常のフォーム送信の場合
- 直前の URL(この例では
/posts/create)へリダイレクトします - エラーメッセージと入力値がセッションにフラッシュされます。フラッシュされた値は次のリクエストで使われたあと消えます
password・password_confirmation・current_passwordはフラッシュされません。パスワード欄は空のまま戻ります
JSON を期待するリクエストの場合
Accept: application/json ヘッダー付きのリクエストや、X-Requested-With: XMLHttpRequest ヘッダー付きの XHR リクエストでは、リダイレクトしません。代わりに HTTP ステータス 422 と、次の形の JSON を返します。
{
"message": "The title field is required. (and 1 more error)",
"errors": {
"title": [
"The title field is required."
],
"body": [
"The body field is required."
]
}
}
message には最初のエラーと残りの件数が入り、errors には項目ごとのエラーメッセージが配列で入ります。
よく使うルールの例
同じタイトルの投稿を登録させたくない場合は unique を使います。
'title' => ['required', 'string', 'max:255', 'unique:posts'],
unique:posts は、posts テーブルに同じ値がないかを確認します。対象のテーブルがマイグレーション済みである必要があります。
ユーザー登録のようなフォームでは、次のルールもよく使います。
$validated = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
email: メールアドレスの形式かを確認しますconfirmed:password_confirmationという名前の項目と値が一致するかを確認します
任意入力の項目には nullable を付けます。Laravel は空の文字列を null に変換するため、nullable がないと空欄が null として検証され、date などのルールで失敗します。
'publish_at' => ['nullable', 'date'],
エラーメッセージの表示
フォームの例
リダイレクトで戻ってきたフォーム画面では、$errors 変数でエラーを表示できます。$errors は web ミドルウェアグループによってすべてのビューに共有されており、エラーがないときも常に定義されています。
resources/views/posts/create.blade.php の例です。
@if (session('status'))
<p>{{ session('status') }}</p>
@endif
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form method="POST" action="/posts">
@csrf
<label for="title">タイトル</label>
<input id="title" type="text" name="title" value="{{ old('title') }}" class="@error('title') is-invalid @enderror">
@error('title')
<div class="field-error">{{ $message }}</div>
@enderror
<label for="body">本文</label>
<textarea id="body" name="body">{{ old('body') }}</textarea>
@error('body')
<div class="field-error">{{ $message }}</div>
@enderror
<button type="submit">保存</button>
</form>
ポイントは次の 4 つです。
@csrf: フォームには必ず入れます。CSRF トークンがない POST リクエストは 419 エラーになります$errors->any()と$errors->all(): エラーをまとめて一覧表示します@error('title')〜@enderror: その項目にエラーがあるときだけ中身を表示します。中では$messageでエラーメッセージを参照できます。class属性の中で使うと、エラー時だけクラスを付けられますold('title'): フラッシュされた入力値を取り出します。エラーで戻ったときも、入力した内容がフォームに残ります
前述のとおりパスワードはフラッシュされないため、パスワード欄に old() は使いません。
フォームリクエスト
ルールが増えてきたときや、複数のアクションで同じルールを使うときは、フォームリクエストに切り出すと便利です。フォームリクエストは、バリデーションと認可(このリクエストを実行してよいかの判定)をまとめた専用のリクエストクラスです。
1. クラスを生成する
php artisan make:request StorePostRequest
app/Http/Requests/StorePostRequest.php が作成されます。生成直後のクラスには、次の 2 つのメソッドがあります。
public function authorize(): bool
{
return false;
}
public function rules(): array
{
return [
//
];
}
2. authorize() を変更する
生成されたクラスの authorize() は false を返します。authorize() が false を返すと 403 エラーになり、コントローラのメソッドは実行されません。生成したまま使うと必ず 403 になるので、最初にここを変更します。
認可を別の場所(ルートのミドルウェアやコントローラ)で行う場合は true を返します。ログイン中のユーザーが操作してよいかをここで確かめたい場合は、ゲートやポリシーを使った判定を書きます。
3. rules() にルールを書く
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'title.required' => 'タイトルは必須です。',
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'body' => '本文',
];
}
}
messages() では「項目名.ルール名」ごとにエラーメッセージを指定できます。attributes() では、メッセージ中の :attribute に入る項目名を指定できます。どちらも必要なときだけ書けば十分です。
4. コントローラで型ヒントする
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class PostController extends Controller
{
public function create(): View
{
return view('posts.create');
}
public function store(StorePostRequest $request): RedirectResponse
{
$validated = $request->validated();
// $validated を使って投稿を保存します
return redirect('/posts/create')->with('status', '投稿を保存しました。');
}
}
Request の代わりに StorePostRequest を型ヒントすると、メソッドが実行される前に認可とバリデーションが行われます。メソッド内に到達した時点で、入力値は検証済みです。失敗時の動き(リダイレクトまたは 422 の JSON)は $request->validate() と同じです。
5. 検証済みデータを取り出す
$request->validated() は、ルールを指定した項目の値だけを配列で返します。一部の項目だけ取り出したいときは safe() を使います。
$titleOnly = $request->safe()->only(['title']);
$exceptBody = $request->safe()->except(['body']);
safe() はフォームリクエストとバリデータのメソッドです。通常の Illuminate\Http\Request にはないため、$request->validate() を使った場合に $request->safe() を呼ぶとエラーになります。その場合は validate() の戻り値を使ってください。
エラーメッセージのカスタマイズと日本語化
validate() の引数で指定する
コントローラで $request->validate() を使う場合は、第 2 引数にメッセージ、第 3 引数に項目名を渡せます。
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
], [
'title.required' => 'タイトルは必須です。',
], [
'body' => '本文',
]);
フォームリクエストを使う場合は、前述の messages() と attributes() に書きます。
言語ファイルで日本語化する
既定のエラーメッセージは英語です。アプリ全体のメッセージを日本語にするには言語ファイルを使います。
Laravel 13.x の新規プロジェクトには lang ディレクトリがありません。まず次のコマンドで言語ファイルを公開します。
php artisan lang:publish
作成されるのは lang/en/ 配下の英語のファイル(auth.php・pagination.php・passwords.php・validation.php)だけで、日本語のファイルは含まれていません。
日本語化の手順は次のとおりです。
lang/en/validation.phpをlang/ja/validation.phpにコピーします- コピーしたファイルのメッセージを日本語に書き換えます
.envのAPP_LOCALEをjaにします
<?php
// lang/ja/validation.php(抜粋)
return [
'required' => ':attributeは必須です。',
'max' => [
'string' => ':attributeは:max文字以内で入力してください。',
],
'custom' => [
'title' => [
'required' => 'タイトルを入力してください。',
],
],
'attributes' => [
'title' => 'タイトル',
'body' => '本文',
],
];
APP_LOCALE=ja
attributes::attributeに入る項目名を指定しますcustom: 特定の項目とルールの組み合わせだけ、個別のメッセージを指定します
lang/ja/validation.php にキーがないメッセージは、フォールバック言語(既定は英語)で表示されます。一方、英語のファイルからコピーして翻訳していない項目は、lang/ja/validation.php の中でも文面が英語なので、英語で表示されます。
まとめ
- Laravel 13.x では、ルールを配列形式で書くのが公式ドキュメントの書き方です。パイプ区切りも動作しますが、
regexに|を含むときは配列形式が必要です - コントローラでは
$request->validate()を使い、成功時も必ずレスポンスを返します - 検証に失敗すると、通常のフォーム送信ではエラーと入力値を持って元のページへリダイレクトし、JSON を期待するリクエストでは 422 の JSON を返します
- フォームには
@csrfを入れ、old()・@error・$errorsでエラーと入力値を表示します - フォームリクエストは
authorize()の変更 →rules()→ 型ヒント →validated()/safe()の順で使います。生成直後のauthorize()はfalseを返すので注意してください - 日本語のメッセージは
lang:publishのあと、lang/ja/validation.phpを自分で用意します