【PHP】Laravelでフォームバリデーションを実装する方法

対象バージョン: 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)だけで、日本語のファイルは含まれていません。

日本語化の手順は次のとおりです。

  1. lang/en/validation.php を lang/ja/validation.php にコピーします
  2. コピーしたファイルのメッセージを日本語に書き換えます
  3. .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 を自分で用意します

参考リンク

コメントする