> ## 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/zh-TW/advanced/custom-validation-rules.md)的基本範例，說明將其作為套件公開 API 維護時的註冊、翻譯與狀態生命週期。

## 與規則物件區分使用

官方文件介紹的 `ValidationRule` 物件也可以在套件中使用。如果「能以字串指定」本身並不是需求，請考慮使用能透過建構子接收具型別參數的物件。

| 方式 | 適合的用途 | 失敗的通知方式 |
| - | - | - |
| 像 `new CustomRule(...)` 這樣的規則物件 | 以類別發佈具型別的設定或複雜的驗證 | 呼叫傳入 `validate()` 的 `$fail` |
| 像 `acme_tracking:AC` 這樣已註冊的字串規則 | 提供也能嵌入字串型規則定義的 API | 註冊的回呼回傳 `false` |

即使同樣名為 `validate()`，呼叫契約也不同。若將 `ValidationRule` 的實作類別直接傳給 `Factory::extend()` 作為類別字串，第 3 個參數的型別與回傳值都不會吻合。

## 區分註冊、建立與驗證

`Validator` Facade 的 `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 載入。關於 Provider 的載入，請參閱[套件自動探索](https://kawax.biz/zh-TW/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;
    }
}
```

回呼會收到 4 個參數：屬性名稱、輸入值、參數陣列，以及正在驗證的 Validator。以 `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 尚未解析時會等待其建立，若已經解析則會立即註冊。關於此 Hook 本身的細節，請參閱[套件的服務解析後 Hook](https://kawax.biz/zh-TW/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 的參數有 5 個：已選擇的訊息、屬性名稱、規則名稱、參數陣列與 Validator。前面的範例省略了未使用的第 5 個參數。

> [!WARNING]
>
> 如果 replacer 總是把整則訊息替換成固定文字，就會忽略使用者透過 `messages()` 或翻譯所做的覆寫。前面的範例只替換傳入之 `$message` 中的 `:prefix`。

若要在應用程式端指定日文的預設文字，請將 `acme_tracking` 加入既有 `lang/ja/validation.php` 的回傳陣列。下方範例只是要新增的項目，並不會刪除其他內建訊息。

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

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

翻譯檔的準備與語系設定請參閱[在地化](https://kawax.biz/zh-TW/localization.md)。若在註冊時執行 `__('...')` 並存為 fallback，Factory 中會留下當時語系的字串。在會切換語系的應用程式中，請使用產生錯誤時的翻譯。

### 使用套件命名空間翻譯的情況

在 `boot()` 中呼叫 `loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier')` 後，就能以 `acme-courier::validation.tracking_code` 參照套件的 `lang/ja/validation.php` 等檔案。

不過，**只把翻譯鍵傳給 `extend()` 的第 3 個參數並不會自動翻譯。** Factory 只會把它當作單純的 fallback 字串保存。將前面 Hook 中的 `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/zh-TW/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`。請把它當作應用於套件專屬欄位間驗證時的範例。

將其加入從 Hook 收到的 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 的註冊。
* 要讓變更反映到長時間執行的 Worker 時，請重新啟動行程，且不要在共用的處理器中殘留輸入或使用者資訊。

關於套件本身的驗證環境，請參閱[套件的測試](https://kawax.biz/zh-TW/advanced/package-testing.md)；關於相容性的處理，也請參閱[套件的版本相容性管理](https://kawax.biz/zh-TW/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/zh-TW/advanced/custom-validation-rules.md)
- [套件的服務解析後掛鉤](https://kawax.biz/zh-TW/advanced/package-service-resolution.md)
- [驗證](https://kawax.biz/zh-TW/validation.md)
- [套件中介層的註冊與適用範圍](https://kawax.biz/zh-TW/advanced/package-middleware.md)
- [套件的郵件傳輸註冊與測試](https://kawax.biz/zh-TW/advanced/package-mail-transports.md)


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