> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# パッケージの文字列バリデーションルール登録

> Laravel 13のFactory::extendを読み解き、文字列ルールの呼び出し契約、エラーメッセージと翻訳、Implicit・Dependentの違い、生成済みValidatorの寿命とテストを解説します。

配布パッケージから `acme_tracking:AC` のような文字列ルールを提供するには、バリデーションの `Factory` に検証処理を登録します。利用アプリケーションのフォームリクエストから、組み込みルールと同じ記法で使えます。

このページではLaravel Framework `v13.40.0` の公式実装を確認しています。[カスタムバリデーションルール](https://kawax.biz/jp/advanced/custom-validation-rules.md)の基本例を補い、パッケージの公開APIとして保守する際の登録・翻訳・状態の寿命を扱います。

## ルールオブジェクトと使い分ける

公式ドキュメントが紹介する `ValidationRule` オブジェクトは、パッケージでも利用できます。文字列で指定できること自体が要件でなければ、コンストラクターで型付きの引数を受け取れるオブジェクトを検討します。

| 方式 | 向いている用途 | 失敗の通知 |
| - | - | - |
| `new CustomRule(...)` のようなルールオブジェクト | 型付き設定や複雑な検証をクラスとして配布する | `validate()` に渡された `$fail` を呼ぶ |
| `acme_tracking:AC` のような登録済み文字列ルール | 文字列ベースのルール定義にも組み込めるAPIを提供する | 登録したコールバックが `false` を返す |

同じ `validate()` という名前でも呼び出し契約が異なります。`ValidationRule` の実装クラスをそのまま `Factory::extend()` のクラス文字列へ渡すと、第3引数の型や戻り値が一致しません。

## 登録・生成・検証を分ける

`Validator` ファサードの `extend()` は、`Illuminate\Validation\Factory` への呼び出しです。入力を検証する `Illuminate\Validation\Validator` インスタンスとは別のオブジェクトです。

```mermaid theme={null}
flowchart TD
    A["パッケージのboot"] --> B["Factoryにルールとreplacerを登録"]
    B --> C["Factory::makeで<br>新しいValidatorを生成"]
    C --> D["登録済みの拡張と<br>fallbackメッセージをコピー"]
    D --> E["各属性のルールを解析し<br>実行条件を確認"]
    E --> F["クラスまたはクロージャで検証"]
    F --> G{"戻り値はtrue?"}
    G -->|Yes| H["このルールは成功"]
    G -->|No| I["メッセージを選択して<br>プレースホルダーを置換"]
```

図は文字列ルールが実行される場合の経路です。未入力や `nullable` などによるスキップは後述します。

Factoryは `validator` というキーのsingletonとして登録されますが、標準実装の `make()` が返すValidatorは毎回新しく生成されます。**Factoryへの後からの登録は、すでに生成されたValidatorの拡張配列を更新しません。**

## パッケージ固有のルールを登録する

例として、英大文字の接頭辞・ハイフン・8桁の数字からなる架空の追跡コードを検証します。`AC-00000001` のような形式の例であり、実在する配送会社の番号仕様ではありません。

パッケージの名前空間 `Acme\Courier\` がComposerのPSR-4で `src/` に対応し、通常の非遅延Service Providerが読み込まれる構成を想定します。プロバイダーの読み込みは[パッケージの自動検出](https://kawax.biz/jp/advanced/package-discovery.md)を参照してください。

### 検証処理をクラスに分離する

`src/Validation/TrackingCode.php` に、文字列ルール用のハンドラーを用意します。このクラスは `ValidationRule` インターフェースを実装しません。

```php theme={null}
<?php

namespace Acme\Courier\Validation;

use Illuminate\Validation\Validator;
use InvalidArgumentException;

final class TrackingCode
{
    public function validate(
        string $attribute,
        mixed $value,
        array $parameters,
        Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || preg_match('/\A[A-Z]{2,8}\z/', $parameters[0]) !== 1
        ) {
            throw new InvalidArgumentException(
                'acme_tracking expects one uppercase ASCII prefix of 2-8 characters.',
            );
        }

        return is_string($value)
            && preg_match(
                '/\A'.preg_quote($parameters[0], '/').'-[0-9]{8}\z/',
                $value,
            ) === 1;
    }
}
```

コールバックへは、属性名・入力値・パラメーター配列・検証中のValidatorの4引数が渡されます。`acme_tracking:AC` の場合、パラメーター配列は `['AC']` です。ルール側でも値の型を確認し、単独で使われても型エラーにしないようにしています。

不正な接頭辞やパラメーター数は、利用者の入力エラーではなく**ルール定義の設定ミス**として例外にしています。空値でルールがスキップされる場合には、この設定チェックも実行されないため、正常な非空値を使ったテストも必要です。

### Factoryの解決後に登録する

`src/CourierServiceProvider.php` の `boot()` から、ルールとメッセージ置換処理を登録します。

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Validation\TrackingCode;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Factory;

final class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('validator', function (Factory $factory): void {
            $factory->extend(
                'acme_tracking',
                TrackingCode::class.'@validate',
                'The :attribute must be :prefix- followed by eight digits.',
            );

            $factory->replacer(
                'acme_tracking',
                static function (
                    string $message,
                    string $attribute,
                    string $rule,
                    array $parameters,
                ): string {
                    return str_replace(':prefix', $parameters[0] ?? '', $message);
                },
            );
        });
    }
}
```

`callAfterResolving()` は、未解決なら生成を待ち、すでに解決されたFactoryにもその場で登録します。フック自体の詳細は[パッケージのサービス解決後フック](https://kawax.biz/jp/advanced/package-service-resolution.md)で解説しています。

クラス文字列 `TrackingCode::class.'@validate'` は、検証時にコンテナでクラスを解決してメソッドを呼び出します。`@validate` を省略したクラス名だけでも既定のメソッド名は `validate` です。コンストラクター依存がある場合は、コンテナで解決できるようにバインドします。

この例にはsingletonのハンドラー登録や入力を保存するプロパティを設けていません。検証時のデータは引数から扱い、Factoryが保持するクロージャへリクエスト固有の状態を取り込まない設計にします。

### 利用アプリケーションから指定する

```php theme={null}
$request->validate([
    'tracking_code' => ['bail', 'required', 'string', 'acme_tracking:AC'],
]);
```

`bail` はその属性の先行ルールが失敗したら後続を止めます。必須性は `required`、形式は `acme_tracking` というように役割を分けます。

公開する名前は `acme_tracking` のような固有のsnake caseで統一します。同じ種類の拡張への同名登録は後の処理が上書きします。また、組み込みの `validateRequired()` などが存在する場合には、そのメソッドが直接呼ばれるため、同名の拡張で組み込み検証を置き換えることはできません。

## 利用者のエラーメッセージを保持する

通常の文字列ルールのメッセージは、おおむね次の優先順で選択されます。サイズルールなどの特殊な分岐はここでは対象外です。

| 優先順 | メッセージの指定元 |
| - | - |
| 1 | `Validator::make()` の第3引数やフォームリクエストの `messages()` |
| 2 | 利用アプリケーションの `validation.custom.属性名.ルール名` の翻訳 |
| 3 | `validation.acme_tracking` のようなルール名の翻訳 |
| 4 | `Factory::extend()` の第3引数に登録したfallback文字列 |

Laravelが `:attribute` などを置換した後に、カスタムreplacerが呼ばれます。replacerの引数は、選択済みメッセージ・属性名・ルール名・パラメーター配列・Validatorの5つです。先の例では利用しない第5引数を省略しています。

> [!WARNING]
>
> replacerでメッセージ全体を常に固定文へ置き換えると、利用者の `messages()` や翻訳の上書きを無視します。先の例は渡された `$message` の `:prefix` だけを置換します。

アプリケーション側で日本語の既定文を指定するには、既存の `lang/ja/validation.php` の返却配列へ `acme_tracking` を追加します。下の例は追加するエントリーの例であり、ほかの組み込みメッセージを削除するものではありません。

```php theme={null}
<?php

return [
    'acme_tracking' => ':attributeは:prefix-に続く8桁の数字で入力してください。',
];
```

翻訳ファイルの準備やロケール設定は[ローカライゼーション](https://kawax.biz/jp/localization.md)を参照してください。登録時に `__('...')` を実行してfallbackへ保存すると、その時点のロケールの文字列がFactoryに残ります。ロケールを切り替えるアプリケーションでは、エラー生成時の翻訳を使います。

### パッケージの名前空間付き翻訳を使う場合

`boot()` で `loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier')` を呼ぶと、パッケージの `lang/ja/validation.php` などを `acme-courier::validation.tracking_code` で参照できます。

ただし、**`extend()` の第3引数へ翻訳キーを渡すだけでは自動翻訳されません。** Factoryは単なるfallback文字列として保持します。先のフック内の `extend()` と `replacer()` を次の登録に置き換えると、fallbackが選ばれたときだけパッケージの翻訳を取得できます。

```php theme={null}
$factory->extend(
    'acme_tracking',
    TrackingCode::class.'@validate',
    'acme-courier::validation.tracking_code',
);

$factory->replacer(
    'acme_tracking',
    static function (
        string $message,
        string $attribute,
        string $rule,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): string {
        if ($message === 'acme-courier::validation.tracking_code') {
            return $validator->getTranslator()->get($message, [
                'attribute' => $validator->getDisplayableAttribute($attribute),
                'prefix' => $parameters[0] ?? '',
            ]);
        }

        return str_replace(':prefix', $parameters[0] ?? '', $message);
    },
);
```

パッケージの各ロケールの `validation.php` に `tracking_code` エントリーを用意します。日本語の文面は先の `acme_tracking` エントリーと同じものを使えます。

ここではreplacer内で新しい翻訳文字列を取得するため、`:attribute` も明示的に翻訳へ渡しています。Laravelの標準置換がこの戻り値に再度適用されるわけではありません。利用者の独自メッセージが選ばれた場合は、その文面を維持します。配布翻訳の上書きは[パッケージ翻訳の上書きと更新](https://kawax.biz/jp/advanced/package-translations.md)を参照してください。

## Implicitとnullableの実行条件を理解する

`extendImplicit()` は、欠落した属性や空文字でも検証候補になるルールを登録します。「必ず失敗させる」「ほかのスキップ条件をすべて無視する」という意味ではありません。

| 入力・指定 | 通常の `extend()` | `extendImplicit()` |
| - | - | - |
| 属性がない | 実行しない | 実行対象になる |
| 空文字・空白だけの文字列 | 実行しない | 実行対象になる |
| 存在する属性の `null`、`nullable` なし | 実行対象になる | 実行対象になる |
| 存在する属性の `null`、`nullable` あり | 実行しない | 実行対象になる |
| 属性がなく、`sometimes` あり | 実行しない | 実行しない |

表は各ルールの実行条件の比較です。`bail`、属性の除外、`stopOnFirstFailure()` などによって先に処理が止まるケースは別です。HTTP入力では空文字がミドルウェアによって `null` に変換されることもあるため、空文字と `null` を別々にテストします。

今回の形式ルールは通常の `extend()` と `required` の組み合わせで十分です。公開済みのルールをImplicitへ変更すると、以前は検証されなかった入力が失敗する可能性があるため、互換性の変更として扱います。

## Dependentルールで配列内の参照先を解決する

別フィールドの名前をパラメーターに取るルールでは、`extendDependent()` が利用できます。`items.*.confirmed_tracking` を検証するとき、パラメーターの `items.*.tracking` が同じ行の `items.0.tracking` などへ変換されます。通常の `extend()` ではこの置換は行われません。

次は、この仕組みを確認するための一致ルールです。単純な一致の検証だけなら、実際のアプリケーションでは組み込みの `same` を使えます。パッケージ固有のフィールド間検証へ応用する際の例として扱ってください。

フックから受け取ったFactoryへ追加します。

```php theme={null}
$factory->extendDependent(
    'acme_matches_field',
    static function (
        string $attribute,
        mixed $value,
        array $parameters,
        \Illuminate\Validation\Validator $validator,
    ): bool {
        if (count($parameters) !== 1
            || ! is_string($parameters[0])
            || $parameters[0] === ''
        ) {
            throw new \InvalidArgumentException('acme_matches_field expects one field name.');
        }

        $data = $validator->getData();

        return \Illuminate\Support\Arr::has($data, $parameters[0])
            && $value === \Illuminate\Support\Arr::get($data, $parameters[0]);
    },
    'The :attribute does not match the related field.',
);
```

```php theme={null}
$request->validate([
    'items' => ['required', 'array'],
    'items.*' => ['array:tracking,confirmed_tracking'],
    'items.*.tracking' => ['required', 'string', 'acme_tracking:AC'],
    'items.*.confirmed_tracking' => [
        'required',
        'string',
        'acme_matches_field:items.*.tracking',
    ],
]);
```

Dependentへの登録は、参照先の存在チェックや検証済み状態を保証しません。第4引数の `getData()` は**バリデーション対象のデータ**であり、検証済みデータだけではありません。参照元と参照先の両方に必要なルールを定義します。

また、DependentにしてもImplicitにはなりません。欠落や空値に対する必須性は別に指定します。実装はパラメーターへドットのエスケープ処理と `*` の置換を行うため、フィールド名以外の任意文字列を混ぜるAPIは避けます。

## アプリケーションで登録から検証までテストする

パッケージとService Providerが読み込まれるLaravel 13アプリケーションで、`tests/Feature/TrackingRuleTest.php` を作成します。クラス単体のテストに加えて、実際のFactory経由で登録とメッセージ処理を検証します。

```php theme={null}
<?php

namespace Tests\Feature;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Factory;
use Tests\TestCase;

final class TrackingRuleTest extends TestCase
{
    public function test_format_presence_and_null_are_separate(): void
    {
        $rules = ['code' => ['acme_tracking:AC']];

        foreach ([
            [['code' => 'AC-00000001'], true],
            [['code' => 'ZX-00000001'], false],
            [['code' => ['AC-00000001']], false],
            [['code' => ''], true],
            [['code' => null], false],
            [[], true],
        ] as [$data, $expected]) {
            $this->assertSame($expected, Validator::make($data, $rules)->passes());
        }

        $this->assertTrue(Validator::make(
            ['code' => null],
            ['code' => ['nullable', 'acme_tracking:AC']],
        )->passes());

        $this->assertFalse(Validator::make(
            [],
            ['code' => ['required', 'acme_tracking:AC']],
        )->passes());
    }

    public function test_application_message_and_display_name_are_kept(): void
    {
        $validator = Validator::make(
            ['code' => 'invalid'],
            ['code' => ['acme_tracking:AC']],
            ['code.acme_tracking' => ':attributeには:prefixのコードを入力してください。'],
            ['code' => '追跡番号'],
        );

        $this->assertSame(
            '追跡番号にはACのコードを入力してください。',
            $validator->errors()->first('code'),
        );
    }

    public function test_existing_validator_keeps_its_extensions(): void
    {
        $factory = new Factory($this->app['translator'], $this->app);
        $factory->extend('acme_probe', static fn (): bool => true);
        $before = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $factory->extend('acme_probe', static fn (): bool => false, 'Rejected.');
        $after = $factory->make(['code' => 'x'], ['code' => ['acme_probe']]);

        $this->assertTrue($before->passes());
        $this->assertFalse($after->passes());
    }
}
```

```bash theme={null}
php artisan test --compact tests/Feature/TrackingRuleTest.php
```

最後のテストは共有Factoryを変更せず、新しいFactoryを作って登録内容のコピーを確認しています。名前空間付き翻訳やDependentの例も提供する場合は、対応する登録を含む環境で追加のテストを用意します。

## 長期保守で確認すること

* ルール名、パラメーターの意味・個数、必須性、受け付ける値の型を公開APIとして記録する。
* 不正なパラメーター、欠落・空文字・`null`、`nullable`・`sometimes`・`bail` の組み合わせを検証する。
* 利用者の `messages()`、属性表示名、ロケール切り替え、パッケージ翻訳の上書きを確認する。
* Dependentルールは複数行とネストした配列で、ほかの行の値を参照しないことを確認する。
* 未解決・解決済みFactoryへの登録を確認し、生成済みValidatorへ遡って反映されることを期待しない。
* 複数パッケージとの名前衝突を避け、同じ名前を通常・Implicit・Dependentの登録に混在させない。
* 長寿命ワーカーへ変更を反映するときはプロセスを再起動し、入力やユーザー情報を共有ハンドラーに残さない。

パッケージ単体の検証環境については[パッケージのテスト](https://kawax.biz/jp/advanced/package-testing.md)、互換性の扱いは[パッケージのバージョン互換性管理](https://kawax.biz/jp/advanced/package-versioning.md)も参照してください。

## 参照した一次情報

* [Laravel公式ドキュメント: カスタムルールとImplicitルール](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/validation.md#custom-validation-rules)
* [Laravel公式ドキュメント: パッケージのService Providerと翻訳](https://github.com/laravel/docs/blob/10add0d2dade04cacfab436dcc3a8df7ca004e74/packages.md)
* [Factory: make・extend・extendImplicit・extendDependent・replacer](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Factory.php)
* [Validator: 実行条件・拡張登録・クラスの解決](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Validator.php)
* [ValidationRuleParser: 文字列ルールの解析](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationRuleParser.php)
* [FormatsMessages: メッセージの優先順と置換](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/Concerns/FormatsMessages.php)
* [ValidationServiceProvider: validatorのsingleton登録](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Validation/ValidationServiceProvider.php)
* [ServiceProvider: callAfterResolving](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php#L310-L317)
* [公式テスト: Factoryから拡張とfallbackの転送](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationFactoryTest.php)
* [公式テスト: 文字列・クラス・Implicit・Dependentルール](https://github.com/laravel/framework/blob/v13.40.0/tests/Validation/ValidationValidatorTest.php#L7372-L7471)


## Related topics

- [カスタムバリデーションルール](https://kawax.biz/jp/advanced/custom-validation-rules.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [パッケージのサービス解決後フック](https://kawax.biz/jp/advanced/package-service-resolution.md)
- [バリデーション](https://kawax.biz/jp/validation.md)
- [パッケージの動的リレーション登録](https://kawax.biz/jp/advanced/package-dynamic-relations.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.