皆さんはどのようなバリデーションライフを送っているでしょうか?
    Laravel には    Illuminate\Foundation\Http\FormRequest    クラスを拡張して使用する方法や、    Illuminate\Http\Request    クラスのvalidateメソッドを使用する方法など様々な方法で実装することができます。
    
いずれの方法で実装するとしても、課題となるのがバリデーションルールの共通化かと思います。
例えば、作成や編集等の処理の違いや、ユーザーのロールによる違い等でバリデーションルールが微妙に異なるが基礎となる部分は共通であることがほとんどかと思います。
    もし愚直に実装するのであれば、例として     Illuminate\Foundation\Http\FormRequest    クラスを拡張して CreateRequest クラスと EditRequest クラスを作成し、それぞれに同じようなバリデーションルールを記述していくことになります。
    
class CreateRequest extends FormRequest {
    public function rules(): array 
    {
        return [
            'text' => ['required', 'string', 'min:5', 'max:100'],
        ];
    }
}

class EditRequest extends FormRequest {
    public function rules(): array 
    {
        return [
            'text' => ['required', 'string', 'min:5', 'max:100'],
        ];
    }
}
    
    ただし、これだと text の最大文字数を120文字に変更したいといった要望があった際にCreateRequest と EditRequest の両方のルールを変更する必要があり、変更もれが発生するリスクが上がってしまいます。
    そこでLaravelが提供しているデフォルトの機能である    Illuminate\Contracts\Validation\ValidationRule    クラスを拡張した独自ルールを作成して、共通化してしまおうというのが今回の記事になります。
    
それでは、前置きが長くなってしまったのでこのぐらいにして実装の説明に進んでいきましょう。
実装
今回は input の name 属性が text である想定で実装していきます。
1. TextRuleクラスを作成
以下のコマンドを実行し TextRule クラスを作成します。
php artisan make:rule TextRule
    
    2. バリデーションを実装
validator 関数を使用してバリデーションを行い、エラーがあった場合は最初のエラーを使用するよう処理しています。
また、最初のエラーのみ使用するため bail ルールを記述し失敗した時点でバリデーションを中止するようにしています。
<?php

namespace App\Rules;

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

class TextRule implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $validator = validator(
            [$attribute => $value],
            [$attribute => ['bail', 'required', 'string', 'min:5', 'max:100']],
        );

        if ($validator->fails()) {
            $fail($validator->errors()->first());
        }
    }
}
    
    3. 必須か任意かを 設定できるようにする
3-1. 必須か任意かのフラグを受け取るよう変更
コンストラクタで必須か否かのフラグを引数として受け取るように変更します。
この時、validateメソッドで必須か否かのフラグを使用できるようプライベートプロパティとして宣言をしています。

class TextRule implements ValidationRule
{
    public function __construct(private readonly bool $required = true)
    {
    }
    // ...
}
    
    3-2. 必須か任意のルールを設定するよう変更
元々、required ルールで固定だった箇所を $required プロパティの値に応じて required か nullable どちらかのルールを付与するように変更します。
class TextRule implements ValidationRule
{
    // ...
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $validator = validator(
            [$attribute => $value],
            [$attribute => [
                'bail',
                $this->required ? 'required' : 'nullable',
                'string',
                'min:5',
                'max:100'
            ]],
        );

        if ($validator->fails()) {
            $fail($validator->errors()->first());
        }
    }
}
    
    4. 属性名の多言語対応
    lang/{locale}/validation.php    へ以下のように記述することで対応できますが、これだとグローバルに設定されてしまうため今回は個別に設定できるようにします。
    
'attributes' => [
    'text' => 'テキスト',
],
    
    4-1. 翻訳ファイルを作成
以下のコマンドを実行して lang ファイルを作成します。
php artisan lang:publish
    
    4-2. 属性名の翻訳を追加
    今回はサンプルなので     lang/en/validation.php    へ以下を追加します。
    
return [
    // ...
    'my_attributes' => [ 
        'text' => 'テキスト',
    ],
]
    
    4-3. 翻訳ファイルを使用するよう TextRule を編集
validator 関数の 第三引数へ空の配列を、第四引数へ属性名と翻訳後の属性名を追加します
class TextRule implements ValidationRule
{
    // ...
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $validator = validator(
            [$attribute => $value],
            [$attribute => [
                'bail',
                $this->required ? 'required' : 'nullable',
                'string',
                'min:5',
                'max:100'
            ]],
            [], 
            [$attribute => __('validation.my_attributes.text')]
        );
        //...
    }
}
    
    5. 検証
テストコードを書いてもいいのですが、今回はエラーの中身を見るために dd関数を使用した方法で検証を行います。
    まず、    routes/web.php    を以下のように変更します。
    
Route::get('/', function () {
    dd(validator(
        ['text' => '1'], // 最小文字数が5文字なのでエラーになるよう1文字だけにする
        ['text' => [new \App\Rules\TextRule()]
    ])->errors());
});
    
    この状態でトップページへアクセスすると、以下のようにエラー内容がブラウザへ表示されるかと思います。
Illuminate\Support\MessageBag {#358 ▼ // routes/web.php:18
  #messages: array:1 [▼
    "text" => array:1 [▼
      0 => "テキストは、5文字以上で指定してください。"
    ]
  ]
  #format: ":message"
}
    
    まとめ
以上でRuleクラスを使用したバリデーションルールの共通化が完了となります。
複雑な条件になってくると対応できない可能性もありますが、コンストラクタで必要な値を受け取ることができるので、ほとんどのユースケースをカバーできるのではないかと考えています。
これで、今後はバリデーションルールの変更漏れによるバグを埋め込むことはなくなり、幸せなバリデーションライフを送れるようになりましたら幸いです。