【Laravel】バリデーションのカスタムルールを作成する方法|Rule::make・Invokable・FormRequest

はじめに

Laravelの標準バリデーションルールは非常に充実していますが、業務ロジックに踏み込んだ検証は自前で実装する必要があります。

たとえば「特定のステータスのときだけ必須になる項目」「DBを参照して重複チェックをカスタマイズしたい」「複数フィールドをまたいだ整合性チェック」などは、標準ルールだけでは対応できません。

この記事では、Laravelのカスタムバリデーションルールの作成方法を3つのアプローチで解説します。

カスタムバリデーションルールが必要な場面

標準ルールで対応できない例をいくつか挙げます。

  • 電話番号のフォーマットチェック(ハイフンあり・なし両対応など)
  • 入力値がマスタテーブルに存在するかチェック(exists ルールより複雑な条件)
  • パスワードの強度チェック(大文字・小文字・数字・記号を含むなど)
  • 日付の前後関係チェック(開始日 < 終了日)
  • 他のフィールドの値に依存した条件付きバリデーション

こういったケースでは、カスタムルールを作成して再利用できる形にしておくのがベストプラクティスです。

アプローチ1:Invokableルール(推奨)

Laravel 9以降で推奨されているアプローチです。make:rule コマンドでクラスを生成します。

クラスの生成

php artisan make:rule StrongPassword

app/Rules/StrongPassword.php が生成されます。

実装

<?php

namespace App\\Rules;

use Closure;
use Illuminate\\Contracts\\Validation\\ValidationRule;

class StrongPassword implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strlen($value) < 8) {
            $fail(':attributeは8文字以上で入力してください。');
            return;
        }

        if (!preg_match('/[A-Z]/', $value)) {
            $fail(':attributeは大文字を1文字以上含めてください。');
            return;
        }

        if (!preg_match('/[0-9]/', $value)) {
            $fail(':attributeは数字を1文字以上含めてください。');
            return;
        }
    }
}

$fail クロージャにメッセージを渡すとバリデーション失敗となります。複数の条件がある場合は return で早期リターンするか、すべての条件を検証してまとめてエラーを返すか、要件に合わせて選択します。

使い方

use App\\Rules\\StrongPassword;

$request->validate([
    'password' => ['required', 'string', new StrongPassword],
]);

アプローチ2:Rule::make を使ったクロージャルール

シンプルな条件であれば、クラスを作らず Rule::make でインラインに書けます。Laravel 10以降で利用可能です。

use Illuminate\\Validation\\Rule;

$request->validate([
    'age' => [
        'required',
        'integer',
        Rule::make(function (string $attribute, mixed $value, Closure $fail) {
            if ($value < 18) {
                $fail(':attributeは18歳以上である必要があります。');
            }
        }),
    ],
]);

クラスを作るほどでもない使い捨てのルールや、コントローラ内で完結させたい場合に便利です。ただし再利用性がないため、複数箇所で使うルールはInvokableクラスにすることをおすすめします。

アプローチ3:Validator::extend を使った方法(旧来の方法)

Laravel 8以前のコードでよく見かける方法です。AppServiceProvider に登録します。

// app/Providers/AppServiceProvider.php

use Illuminate\\Support\\Facades\\Validator;

public function boot(): void
{
    Validator::extend('strong_password', function ($attribute, $value, $parameters, $validator) {
        return strlen($value) >= 8 && preg_match('/[A-Z]/', $value);
    });
}

使い方はルール名の文字列で指定します。

$request->validate([
    'password' => ['required', 'strong_password'],
]);

エラーメッセージは resources/lang/ja/validation.php に追加します。

'strong_password' => ':attributeは8文字以上かつ大文字を含める必要があります。',

現在はInvokableルールが推奨されているため、新規実装ではこの方法は使わなくてよいでしょう。


FormRequest でカスタムルールを活用する

コントローラをシンプルに保ちたい場合は、FormRequest にまとめるのがおすすめです。

FormRequest の生成

php artisan make:request StoreUserRequest

実装

<?php

namespace App\\Http\\Requests;

use App\\Rules\\StrongPassword;
use Illuminate\\Foundation\\Http\\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'unique:users'],
            'password' => ['required', 'string', 'confirmed', new StrongPassword],
        ];
    }

    public function messages(): array
    {
        return [
            'name.required'  => '名前は必須です。',
            'email.unique'   => 'このメールアドレスはすでに使われています。',
        ];
    }
}

コントローラ

public function store(StoreUserRequest $request): RedirectResponse
{
    // バリデーション済みのデータを取得
    $validated = $request->validated();

    User::create($validated);

    return redirect()->route('users.index');
}

FormRequest を使うことで、コントローラにバリデーションロジックが入り込まずスッキリします。

エラーメッセージのカスタマイズ

:attribute プレースホルダー

エラーメッセージ内で :attribute を使うと、バリデーション対象のフィールド名に置き換えられます。

$fail(':attributeは8文字以上で入力してください。');
// → 「パスワードは8文字以上で入力してください。」

フィールド名の日本語化は resources/lang/ja/validation.phpattributes セクションで設定します。

'attributes' => [
    'password' => 'パスワード',
    'email'    => 'メールアドレス',
],

複数のエラーメッセージを返す

デフォルトでは $fail を複数回呼ぶと最初のエラーのみ表示されます。複数のエラーをまとめて返したい場合は、Invokable ルールに $this->implicit() などを活用するより、条件ごとに $fail を分けて呼ぶのがシンプルです。

public function validate(string $attribute, mixed $value, Closure $fail): void
{
    $errors = [];

    if (strlen($value) < 8) {
        $errors[] = '8文字以上';
    }
    if (!preg_match('/[A-Z]/', $value)) {
        $errors[] = '大文字を含む';
    }
    if (!preg_match('/[0-9]/', $value)) {
        $errors[] = '数字を含む';
    }

    if (!empty($errors)) {
        $fail(':attributeは' . implode('、', $errors) . '必要があります。');
    }
}

暗黙のルール(Implicit Rule)

通常のバリデーションルールは、値が空(null・空文字)のときはスキップされます。空値に対してもルールを適用したい場合は ImplicitRule インターフェースを実装します。

use Illuminate\\Contracts\\Validation\\ImplicitRule;

class RequiredIfPremium implements ValidationRule, ImplicitRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 値が空でも実行される
        if (empty($value)) {
            $fail(':attributeは必須です。');
        }
    }
}

まとめ

アプローチ推奨場面
Invokableルール(クラス)再利用する・複雑なロジック・テストしたい
Rule::make(クロージャ)使い捨て・シンプルな条件・インラインで完結
Validator::extend既存の旧コードとの互換性維持(新規非推奨)

基本はInvokableルールを使い、FormRequestと組み合わせることでコントローラをシンプルに保てます。

カスタムルールはテストも書きやすいため、複雑なバリデーションロジックほど積極的にクラスに切り出すことをおすすめします。

LaravelLaravel,PHP

Posted by 千原 耕司