acme_tracking:AC のような文字列ルールを提供するには、バリデーションの Factory に検証処理を登録します。利用アプリケーションのフォームリクエストから、組み込みルールと同じ記法で使えます。
このページではLaravel Framework v13.40.0 の公式実装を確認しています。カスタムバリデーションルールの基本例を補い、パッケージの公開APIとして保守する際の登録・翻訳・状態の寿命を扱います。
ルールオブジェクトと使い分ける
公式ドキュメントが紹介するValidationRule オブジェクトは、パッケージでも利用できます。文字列で指定できること自体が要件でなければ、コンストラクターで型付きの引数を受け取れるオブジェクトを検討します。
同じ
validate() という名前でも呼び出し契約が異なります。ValidationRule の実装クラスをそのまま Factory::extend() のクラス文字列へ渡すと、第3引数の型や戻り値が一致しません。
登録・生成・検証を分ける
Validator ファサードの extend() は、Illuminate\Validation\Factory への呼び出しです。入力を検証する Illuminate\Validation\Validator インスタンスとは別のオブジェクトです。
図は文字列ルールが実行される場合の経路です。未入力や nullable などによるスキップは後述します。
Factoryは validator というキーのsingletonとして登録されますが、標準実装の make() が返すValidatorは毎回新しく生成されます。Factoryへの後からの登録は、すでに生成されたValidatorの拡張配列を更新しません。
パッケージ固有のルールを登録する
例として、英大文字の接頭辞・ハイフン・8桁の数字からなる架空の追跡コードを検証します。AC-00000001 のような形式の例であり、実在する配送会社の番号仕様ではありません。
パッケージの名前空間 Acme\Courier\ がComposerのPSR-4で src/ に対応し、通常の非遅延Service Providerが読み込まれる構成を想定します。プロバイダーの読み込みはパッケージの自動検出を参照してください。
検証処理をクラスに分離する
src/Validation/TrackingCode.php に、文字列ルール用のハンドラーを用意します。このクラスは ValidationRule インターフェースを実装しません。
acme_tracking:AC の場合、パラメーター配列は ['AC'] です。ルール側でも値の型を確認し、単独で使われても型エラーにしないようにしています。
不正な接頭辞やパラメーター数は、利用者の入力エラーではなくルール定義の設定ミスとして例外にしています。空値でルールがスキップされる場合には、この設定チェックも実行されないため、正常な非空値を使ったテストも必要です。
Factoryの解決後に登録する
src/CourierServiceProvider.php の boot() から、ルールとメッセージ置換処理を登録します。
callAfterResolving() は、未解決なら生成を待ち、すでに解決されたFactoryにもその場で登録します。フック自体の詳細はパッケージのサービス解決後フックで解説しています。
クラス文字列 TrackingCode::class.'@validate' は、検証時にコンテナでクラスを解決してメソッドを呼び出します。@validate を省略したクラス名だけでも既定のメソッド名は validate です。コンストラクター依存がある場合は、コンテナで解決できるようにバインドします。
この例にはsingletonのハンドラー登録や入力を保存するプロパティを設けていません。検証時のデータは引数から扱い、Factoryが保持するクロージャへリクエスト固有の状態を取り込まない設計にします。
利用アプリケーションから指定する
bail はその属性の先行ルールが失敗したら後続を止めます。必須性は required、形式は acme_tracking というように役割を分けます。
公開する名前は acme_tracking のような固有のsnake caseで統一します。同じ種類の拡張への同名登録は後の処理が上書きします。また、組み込みの validateRequired() などが存在する場合には、そのメソッドが直接呼ばれるため、同名の拡張で組み込み検証を置き換えることはできません。
利用者のエラーメッセージを保持する
通常の文字列ルールのメッセージは、おおむね次の優先順で選択されます。サイズルールなどの特殊な分岐はここでは対象外です。
Laravelが
:attribute などを置換した後に、カスタムreplacerが呼ばれます。replacerの引数は、選択済みメッセージ・属性名・ルール名・パラメーター配列・Validatorの5つです。先の例では利用しない第5引数を省略しています。
アプリケーション側で日本語の既定文を指定するには、既存の lang/ja/validation.php の返却配列へ acme_tracking を追加します。下の例は追加するエントリーの例であり、ほかの組み込みメッセージを削除するものではありません。
__('...') を実行してfallbackへ保存すると、その時点のロケールの文字列がFactoryに残ります。ロケールを切り替えるアプリケーションでは、エラー生成時の翻訳を使います。
パッケージの名前空間付き翻訳を使う場合
boot() で loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier') を呼ぶと、パッケージの lang/ja/validation.php などを acme-courier::validation.tracking_code で参照できます。
ただし、extend() の第3引数へ翻訳キーを渡すだけでは自動翻訳されません。 Factoryは単なるfallback文字列として保持します。先のフック内の extend() と replacer() を次の登録に置き換えると、fallbackが選ばれたときだけパッケージの翻訳を取得できます。
validation.php に tracking_code エントリーを用意します。日本語の文面は先の acme_tracking エントリーと同じものを使えます。
ここではreplacer内で新しい翻訳文字列を取得するため、:attribute も明示的に翻訳へ渡しています。Laravelの標準置換がこの戻り値に再度適用されるわけではありません。利用者の独自メッセージが選ばれた場合は、その文面を維持します。配布翻訳の上書きはパッケージ翻訳の上書きと更新を参照してください。
Implicitとnullableの実行条件を理解する
extendImplicit() は、欠落した属性や空文字でも検証候補になるルールを登録します。「必ず失敗させる」「ほかのスキップ条件をすべて無視する」という意味ではありません。
表は各ルールの実行条件の比較です。
bail、属性の除外、stopOnFirstFailure() などによって先に処理が止まるケースは別です。HTTP入力では空文字がミドルウェアによって null に変換されることもあるため、空文字と null を別々にテストします。
今回の形式ルールは通常の extend() と required の組み合わせで十分です。公開済みのルールをImplicitへ変更すると、以前は検証されなかった入力が失敗する可能性があるため、互換性の変更として扱います。
Dependentルールで配列内の参照先を解決する
別フィールドの名前をパラメーターに取るルールでは、extendDependent() が利用できます。items.*.confirmed_tracking を検証するとき、パラメーターの items.*.tracking が同じ行の items.0.tracking などへ変換されます。通常の extend() ではこの置換は行われません。
次は、この仕組みを確認するための一致ルールです。単純な一致の検証だけなら、実際のアプリケーションでは組み込みの same を使えます。パッケージ固有のフィールド間検証へ応用する際の例として扱ってください。
フックから受け取ったFactoryへ追加します。
getData() はバリデーション対象のデータであり、検証済みデータだけではありません。参照元と参照先の両方に必要なルールを定義します。
また、DependentにしてもImplicitにはなりません。欠落や空値に対する必須性は別に指定します。実装はパラメーターへドットのエスケープ処理と * の置換を行うため、フィールド名以外の任意文字列を混ぜるAPIは避けます。
アプリケーションで登録から検証までテストする
パッケージとService Providerが読み込まれるLaravel 13アプリケーションで、tests/Feature/TrackingRuleTest.php を作成します。クラス単体のテストに加えて、実際のFactory経由で登録とメッセージ処理を検証します。
長期保守で確認すること
- ルール名、パラメーターの意味・個数、必須性、受け付ける値の型を公開APIとして記録する。
- 不正なパラメーター、欠落・空文字・
null、nullable・sometimes・bailの組み合わせを検証する。 - 利用者の
messages()、属性表示名、ロケール切り替え、パッケージ翻訳の上書きを確認する。 - Dependentルールは複数行とネストした配列で、ほかの行の値を参照しないことを確認する。
- 未解決・解決済みFactoryへの登録を確認し、生成済みValidatorへ遡って反映されることを期待しない。
- 複数パッケージとの名前衝突を避け、同じ名前を通常・Implicit・Dependentの登録に混在させない。
- 長寿命ワーカーへ変更を反映するときはプロセスを再起動し、入力やユーザー情報を共有ハンドラーに残さない。
参照した一次情報
- Laravel公式ドキュメント: カスタムルールとImplicitルール
- Laravel公式ドキュメント: パッケージのService Providerと翻訳
- Factory: make・extend・extendImplicit・extendDependent・replacer
- Validator: 実行条件・拡張登録・クラスの解決
- ValidationRuleParser: 文字列ルールの解析
- FormatsMessages: メッセージの優先順と置換
- ValidationServiceProvider: validatorのsingleton登録
- ServiceProvider: callAfterResolving
- 公式テスト: Factoryから拡張とfallbackの転送
- 公式テスト: 文字列・クラス・Implicit・Dependentルール