跳转到主要内容

什么是自定义验证规则

Laravel 已内置丰富的验证规则,但在实际开发中经常需要应用特有的校验逻辑。使用自定义验证规则可以将可复用的校验逻辑定义为类或闭包,并像标准规则一样使用。 定义自定义规则主要有两种方式。
  • 规则对象 — 可复用性高、易于测试
  • 闭包 — 适合仅使用一次的简单规则

规则对象

生成规则类

使用 make:rule Artisan 命令生成新的规则类。生成的类会放在 app/Rules 目录下。
php artisan make:rule Uppercase

实现 ValidationRule 接口

在生成的类中实现 validate 方法。校验失败时通过调用 $fail 闭包报告错误。
<?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 闭包的字符串中可以使用 :attribute 占位符,Laravel 会将其替换为字段名。

应用规则对象

将规则对象的实例传入校验数组即可。
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
在 Form Request 的 rules() 方法中也可以同样使用。
public function rules(): array
{
    return [
        'name' => ['required', 'string', new Uppercase],
    ];
}

使用翻译键的错误消息

除了硬编码错误消息,也可以使用翻译键。
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (strtoupper($value) !== $value) {
        $fail('validation.uppercase')->translate();
    }
}
在翻译文件 lang/zh_CN/validation.php 中添加消息:
return [
    'uppercase' => ':attribute 必须为大写。',
    // ...
];

添加多条错误消息

要为同一字段报告多条错误,可以多次调用 $fail
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.');
    }
}

基于闭包的规则

对于在应用中仅使用一次的简单规则,可以不创建类,直接用闭包定义。
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.");
            }
        },
    ],
]);
闭包规则可以内联定义,非常轻量,但难以复用或独立测试。因此,多个地方共用的逻辑推荐拆分为规则对象。

Implicit 规则(值为空时也执行)

默认情况下,当字段为空或不存在时,自定义规则不会被执行。若希望即使值为空也执行规则,生成类时可加上 --implicit 选项。
php artisan make:rule Uppercase --implicit
生成的类会实现 ImplicitRule 接口。该接口本身没有额外方法,仅作为向 Laravel 传递的信号。
<?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.');
        }
    }
}
ImplicitRule 只是告诉 Laravel「该属性是必需的」。当值为空时是否真的判定为验证失败,取决于 validate 方法的实现。

数据访问

DataAwareRule — 访问整个表单的数据

如果需要基于其他字段的值来校验,可实现 DataAwareRule 接口。校验开始前 setData 方法会被自动调用。
<?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.');
        }
    }
}
使用示例:
$request->validate([
    'tenant_id' => 'required|integer',
    'email' => ['required', 'email', new UniqueForTenant('users', 'email')],
]);

ValidatorAwareRule — 访问 Validator 实例

若要访问 Validator 所拥有的全部信息(失败的规则、自定义消息等),可实现 ValidatorAwareRule 接口。
<?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.');
        }
    }
}

实战用例

中文字符检查

对全角/半角字符类型进行校验的规则。
php artisan make:rule ChineseOnly
<?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
    {
        // 仅允许汉字及常见标点
        if (! preg_match('/^[\p{Han}\s]+$/u', $value)) {
            $fail(':attribute 必须只包含中文字符。');
        }
    }
}
$request->validate([
    'name_cn' => ['required', 'string', new ChineseOnly],
]);

电话号码格式校验

校验中国大陆电话号码格式的规则。
php artisan make:rule ChinesePhone
<?php

namespace App\Rules;

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

class ChinesePhone implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 支持带/不带分隔符,同时允许国际形式
        $normalized = preg_replace('/[\s\-\(\)]/', '', $value);

        $patterns = [
            '/^1[3-9]\d{9}$/',        // 中国大陆手机号
            '/^\+861[3-9]\d{9}$/',    // 国际形式
        ];

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

        $fail(':attribute 必须为有效的电话号码格式。');
    }
}

带租户 ID 的唯一性约束

多租户应用中常见的租户内唯一性约束。
1

创建规则类

php artisan make:rule TenantUnique
<?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 已被占用。');
        }
    }
}
2

在 Form Request 中使用

<?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;
    }
}
3

更新时排除自身 ID

更新已有记录时,需将自身 ID 排除后进行重复检查。
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),
            ],
        ];
    }
}

在 ServiceProvider 中注册规则

使用 Validator::extend() 追加规则

使用 Validator::extend() 可以让自定义规则以字符串形式('rule_name')使用。在 AppServiceProviderboot() 方法中注册。
<?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() 注册的规则可以用字符串指定。
$request->validate([
    'name_cn' => 'required|chinese_only',
]);
Validator::extend() 是比规则对象更早的注册方式。新项目建议使用实现了 ValidationRule 接口的规则对象。

作为静态方法追加到 Rule 类

通过给 Rule Facade 添加宏,可以提供 Rule::myRule() 这样的流畅 API(Fluent API)。
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
// 使用示例
$request->validate([
    'name' => ['required', Rule::tenantUnique('projects', 'name')],
]);

内部实现细节

来看看 Illuminate\Validation\Validator 是如何调用自定义规则的。 Validator 内部由 validateAttribute() 处理每个字段。若规则实现了 ValidationRule 接口,则会调用 validateUsingCustomRule() 方法。
// 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 闭包 — 添加错误消息
        $this->errors()->add($attribute, $this->makeReplacements(
            $message,
            $attribute,
            get_class($rule),
            [], // 追加的占位符替换(例如 ['min' => '8'])
        ));
    });
}
ImplicitRule 的处理通过 isImplicit() 方法判断,即便值为空也会执行规则检查。
// 判断是否对空值执行规则
protected function isImplicit($rule): bool
{
    return $rule instanceof ImplicitRule
        || in_array($rule, $this->implicitRules);
}
可以同时实现 DataAwareRuleValidatorAwareRule。同时实现两个接口的类会同时注入这两者。

相关页面

验证(入门)

了解在 Controller 与 Form Request 中的标准验证方法。
最后修改于 2026年7月13日