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

# 自訂驗證規則

> 從框架內部實作層面說明如何以規則物件或 closure 擴充 Laravel 的驗證功能。

## 什麼是自訂驗證規則

Laravel 內建了豐富的驗證規則，但仍有需要應用程式專屬檢驗邏輯的場合。透過自訂驗證規則，可以將可重複利用的驗證邏輯定義為類別或 closure，並與標準規則同樣的方式使用。

定義自訂規則主要有兩種方式。

* **規則物件（Rule Object）** — 可重複利用性高、易於測試
* **Closure** — 適合只使用一次的簡易規則

## 規則物件

### 產生規則類別

以 `make:rule` Artisan 指令產生新的規則類別。產生的類別會置於 `app/Rules` 目錄下。

```bash theme={null}
php artisan make:rule Uppercase
```

### 實作 ValidationRule 介面

於產生的類別中實作 `validate` 方法。此方法在驗證失敗時呼叫 `$fail` closure。

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

namespace App\Rules;

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

class Uppercase implements ValidationRule
{
    /**
     * 執行驗證規則
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
```

在傳給 `$fail` closure 的字串中可以使用 `:attribute` 佔位符。Laravel 會將其替換為欄位名稱。

### 套用規則物件

將規則物件的實例傳入驗證陣列即可。

```php theme={null}
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
```

在 Form Request 的 `rules()` 方法中同樣可以使用。

```php theme={null}
public function rules(): array
{
    return [
        'name' => ['required', 'string', new Uppercase],
    ];
}
```

### 使用翻譯 key 的錯誤訊息

除了硬編碼錯誤訊息外，也可以使用翻譯 key。

```php theme={null}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (strtoupper($value) !== $value) {
        $fail('validation.uppercase')->translate();
    }
}
```

在翻譯檔 `lang/zh_TW/validation.php` 中加入訊息。

```php theme={null}
return [
    'uppercase' => ':attribute 必須以大寫輸入。',
    // ...
];
```

### 加入多個錯誤訊息

若要對單一欄位回報多個錯誤，可多次呼叫 `$fail`。

```php theme={null}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (! is_string($value)) {
        $fail('The :attribute must be a string.');
        return;
    }

    if (strlen($value) < 8) {
        $fail('The :attribute must be at least 8 characters.');
    }

    if (! preg_match('/[A-Z]/', $value)) {
        $fail('The :attribute must contain at least one uppercase letter.');
    }
}
```

## 基於 Closure 的規則

在應用程式中僅使用一次的簡單規則，可不建立類別而以 closure 定義。

```php theme={null}
use Illuminate\Support\Facades\Validator;
use Closure;

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value === 'foo') {
                $fail("The {$attribute} is invalid.");
            }
        },
    ],
]);
```

Closure 規則以行內方式定義故非常方便，但重複利用及獨立測試較不便，建議將於多處使用的邏輯抽取為規則物件。

## Implicit 規則（值為空亦執行）

預設情況下，若欄位為空或不存在，自訂規則不會執行。若希望對空值也執行規則，可加上 `--implicit` 選項產生類別。

```bash theme={null}
php artisan make:rule Uppercase --implicit
```

產生的類別實作了 `ImplicitRule` 介面。此介面本身不含額外方法，僅作為對 Laravel 傳送訊號之用。

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

namespace App\Rules;

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

class RequiredIfJapanese implements ValidationRule, ImplicitRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (empty($value)) {
            $fail('The :attribute is required.');
            return;
        }

        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー]+$/u', $value)) {
            $fail('The :attribute must contain only Japanese characters.');
        }
    }
}
```

<Warning>
  `ImplicitRule` 僅向 Laravel 表明「屬性為必要」。當值為空時是否實際讓驗證失敗，取決於 `validate` 方法的實作。
</Warning>

## 資料存取

### DataAwareRule — 存取整個表單的資料

若希望依其他欄位的值進行驗證，可實作 `DataAwareRule` 介面。`setData` 方法會在驗證開始前自動被呼叫。

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

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class UniqueForTenant implements DataAwareRule, ValidationRule
{
    /**
     * 驗證對象的所有資料
     *
     * @var array<string, mixed>
     */
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column = 'value',
    ) {}

    /**
     * 設定驗證資料
     *
     * @param  array<string, mixed>  $data
     */
    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $exists = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value)
            ->exists();

        if ($exists) {
            $fail('The :attribute has already been taken for this tenant.');
        }
    }
}
```

使用範例：

```php theme={null}
$request->validate([
    'tenant_id' => 'required|integer',
    'email' => ['required', 'email', new UniqueForTenant('users', 'email')],
]);
```

### ValidatorAwareRule — 存取 Validator 實例

若要存取 Validator 所擁有的所有資訊（失敗的規則、自訂訊息等），可實作 `ValidatorAwareRule` 介面。

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

namespace App\Rules;

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

class ConditionalFormat implements ValidationRule, ValidatorAwareRule
{
    protected Validator $validator;

    public function setValidator(Validator $validator): static
    {
        $this->validator = $validator;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 若其他欄位已驗證失敗則跳過
        if ($this->validator->errors()->has('type')) {
            return;
        }

        $type = $this->validator->getData()['type'] ?? null;

        if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) {
            $fail('The :attribute must be a valid phone number.');
        }

        if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $fail('The :attribute must be a valid email address.');
        }
    }
}
```

## 實務應用情境

### 中文字元檢查

驗證全形/半形字元的規則。

```bash theme={null}
php artisan make:rule ChineseOnly
```

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

namespace App\Rules;

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

class ChineseOnly implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 僅允許中文字（含 CJK 統一漢字）
        if (! preg_match('/^[\p{Han}\s]+$/u', $value)) {
            $fail(':attribute 請以中文輸入。');
        }
    }
}
```

```php theme={null}
$request->validate([
    'name' => ['required', 'string', new ChineseOnly],
]);
```

### 電話號碼格式驗證

驗證台灣電話號碼格式的規則。

```bash theme={null}
php artisan make:rule TaiwanPhone
```

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

namespace App\Rules;

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

class TaiwanPhone implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 支援有無連字號、也允許國際格式
        $normalized = preg_replace('/[\s\-\(\)]/', '', $value);

        $patterns = [
            '/^0\d{8,9}$/',        // 一般市話、行動電話號碼
            '/^\+886\d{8,9}$/',    // 國際格式
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $normalized)) {
                return;
            }
        }

        $fail(':attribute 請以有效的電話號碼格式輸入。');
    }
}
```

### 帶有租戶 ID 的唯一性約束

多租戶應用程式中常見的、限於租戶範圍內的唯一性約束。

<Steps>
  <Step title="建立規則類別">
    ```bash theme={null}
    php artisan make:rule TenantUnique
    ```

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

    namespace App\Rules;

    use Closure;
    use Illuminate\Contracts\Validation\DataAwareRule;
    use Illuminate\Contracts\Validation\ValidationRule;
    use Illuminate\Support\Facades\DB;

    class TenantUnique implements DataAwareRule, ValidationRule
    {
        protected array $data = [];

        public function __construct(
            protected string $table,
            protected string $column,
            protected ?int $ignoreId = null,
        ) {}

        public function setData(array $data): static
        {
            $this->data = $data;

            return $this;
        }

        public function validate(string $attribute, mixed $value, Closure $fail): void
        {
            $tenantId = $this->data['tenant_id'] ?? null;

            $query = DB::table($this->table)
                ->where('tenant_id', $tenantId)
                ->where($this->column, $value);

            if ($this->ignoreId !== null) {
                $query->where('id', '!=', $this->ignoreId);
            }

            if ($query->exists()) {
                $fail(':attribute 已被使用。');
            }
        }
    }
    ```
  </Step>

  <Step title="於 Form Request 使用">
    ```php theme={null}
    <?php

    namespace App\Http\Requests;

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

    class CreateProjectRequest extends FormRequest
    {
        public function rules(): array
        {
            return [
                'tenant_id' => 'required|integer|exists:tenants,id',
                'name' => [
                    'required',
                    'string',
                    'max:100',
                    new TenantUnique('projects', 'name'),
                ],
            ];
        }

        public function authorize(): bool
        {
            return true;
        }
    }
    ```
  </Step>

  <Step title="更新時排除 ID">
    更新既有資料時，需排除自身的 ID 進行重複檢查。

    ```php theme={null}
    class UpdateProjectRequest extends FormRequest
    {
        public function rules(): array
        {
            $projectId = $this->route('project');

            return [
                'tenant_id' => 'required|integer|exists:tenants,id',
                'name' => [
                    'required',
                    'string',
                    'max:100',
                    new TenantUnique('projects', 'name', ignoreId: $projectId),
                ],
            ];
        }
    }
    ```
  </Step>
</Steps>

## 於服務提供者註冊規則

### 以 Validator::extend() 新增規則

使用 `Validator::extend()` 可以以字串形式（`'rule_name'`）使用自訂規則。於 `AppServiceProvider` 的 `boot()` 方法中註冊。

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

namespace App\Providers;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Validator as ValidatorInstance;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::extend('chinese_only', function (string $attribute, mixed $value, array $parameters, ValidatorInstance $validator): bool {
            return preg_match('/^[\p{Han}\s]+$/u', $value) === 1;
        });

        Validator::replacer('chinese_only', function (string $message, string $attribute): string {
            return str_replace(':attribute', $attribute, ':attribute 請以中文輸入。');
        });
    }
}
```

以 `Validator::extend()` 註冊的規則可以字串形式指定。

```php theme={null}
$request->validate([
    'name' => 'required|chinese_only',
]);
```

<Warning>
  `Validator::extend()` 是比規則物件更舊的註冊方式。新專案建議使用實作 `ValidationRule` 介面的規則物件。
</Warning>

### 以靜態方法加入 Rule 類別

透過對 `Rule` Facade 新增 macro，可提供類似 `Rule::myRule()` 的流暢 API（Fluent API）。

```php theme={null}
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
```

```php theme={null}
// 使用範例
$request->validate([
    'name' => ['required', Rule::tenantUnique('projects', 'name')],
]);
```

## 內部實作細節

讓我們看看 `Illuminate\Validation\Validator` 如何呼叫自訂規則。

在 Validator 中，`validateAttribute()` 會處理各欄位。若規則實作 `ValidationRule` 介面，則會呼叫 `validateUsingCustomRule()` 方法。

```php theme={null}
// Illuminate\Validation\Validator::validateUsingCustomRule() 的簡略版
protected function validateUsingCustomRule($attribute, $value, $rule)
{
    // 若為 DataAwareRule，注入全部資料
    if ($rule instanceof DataAwareRule) {
        $rule->setData($this->getData());
    }

    // 若為 ValidatorAwareRule，注入 Validator 自身
    if ($rule instanceof ValidatorAwareRule) {
        $rule->setValidator($this);
    }

    // 呼叫 validate()
    $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) {
        // $fail closure — 加入錯誤訊息
        $this->errors()->add($attribute, $this->makeReplacements(
            $message,
            $attribute,
            get_class($rule),
            [], // 額外的佔位符替換（例如：['min' => '8'] 等）
        ));
    });
}
```

`ImplicitRule` 的處理由 `isImplicit()` 方法判斷，即便值為空亦會執行規則檢查。

```php theme={null}
// 判斷是否對空值執行規則
protected function isImplicit($rule): bool
{
    return $rule instanceof ImplicitRule
        || in_array($rule, $this->implicitRules);
}
```

<Tip>
  `DataAwareRule` 與 `ValidatorAwareRule` 可以同時實作。若類別同時擁有兩個介面，兩者皆會進行注入。
</Tip>

## 相關頁面

<Card title="驗證（入門）" icon="shield-check" href="/zh-TW/validation">
  查看 Controller 及 Form Request 中的標準驗證方式。
</Card>


## Related topics

- [驗證](/zh-TW/validation.md)
- [自訂驗證 Guard 的實作](/zh-TW/advanced/custom-auth-guard.md)
- [Laravel Prompts](/zh-TW/prompts.md)
- [HTTP 測試](/zh-TW/http-tests.md)
- [Precognition](/zh-TW/precognition.md)
