Filament 表单验证完全指南:字段级校验规则、前端实时反馈与数据库唯一性检查
【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps & admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filament
表单验证是 Filament(Laravel + Livewire 开源 UI 框架)构建后台应用时保障数据质量的第一道防线。本文以 packages/forms/docs/23-validation.md 为骨架,系统讲解 Filament 表单字段支持的近 50 种内置校验方法、字段间比较、数据库唯一性/存在性校验(含scopedUnique/scopedExists的全局作用域处理)、自定义规则与错误消息定制,并深入CanBeValidatedtrait 源码,说明这些声明式 API 最终如何被编译为 Laravel Validator 规则并在前端实时生效。读完本文,你将能够在任意 Filament 表单字段上组合出完整、安全、可复用的验证逻辑。
一、为什么需要字段级验证
在 Laravel 中,验证规则通常以数组['required', 'max:255']或组合字符串required|max:255的形式定义,这在后端配合 FormRequest 使用时没有问题。但 Filament 更进一步:它能够把验证规则同步到前端,让用户在发起任何后端请求之前就能看到错误提示并即时修正,提升表单交互体验。
Filament 为字段提供了两类能力:
- 专用验证方法(即下文"可用规则"一节),例如
->required()、->email(); - 透传 Laravel 原生规则,包括任意 其他 Laravel 验证规则 与 自定义规则。
⚠️ 注意:某些默认的 Laravel 验证规则依赖正确的 attribute 名称,直接通过
rule()/rules()传入时可能无法正常工作。只要能用专用验证方法,就优先使用专用方法。
二、内置可用规则(Available Rules)
所有专用验证方法都定义在 packages/forms/src/Components/Concerns/CanBeValidated.php 的CanBeValidatedtrait 中。以下按用途分组,逐一说明语义、参数与代码示例。
2.1 基础格式类规则
| 方法 | 校验语义 |
|---|---|
activeUrl() | 根据 PHPdns_get_record()判断字段必须拥有有效的 A 或 AAAA 记录(真实可访问的域名) |
alpha() | 字段必须全部为字母字符 |
alphaDash() | 字段可包含字母数字字符,以及短横线-和下划线_ |
alphaNum() | 字段必须全部为字母数字字符 |
ascii() | 字段必须全部为 7 位 ASCII 字符 |
json() | 字段必须是合法的 JSON 字符串 |
hexColor() | 字段必须是合法的十六进制颜色值 |
macAddress() | 字段必须是合法的 MAC 地址 |
string() | 字段必须是字符串 |
ulid() | 字段必须是合法的 ULID(通用唯一字典序可排序标识符) |
uuid() | 字段必须是合法的 RFC 4122(版本 1、3、4 或 5)UUID |
ip()/ipv4()/ipv6() | 字段必须是 IP 地址 / IPv4 / IPv6 地址 |
regex()/notRegex() | 字段必须匹配 / 不得匹配给定的正则表达式 |
基础格式规则的使用示例:
Field::make('name')->activeUrl() Field::make('name')->alpha() Field::make('name')->alphaDash() Field::make('name')->alphaNum() Field::make('name')->ascii() Field::make('ip_address')->ip() Field::make('ip_address')->ipv4() Field::make('ip_address')->ipv6() Field::make('ip_address')->json() Field::make('mac_address')->macAddress() Field::make('number')->multipleOf(2) Field::make('email')->regex('/^.+@.+$/i') Field::make('email')->notRegex('/^.+$/i') Field::make('identifier')->ulid() Field::make('identifier')->uuid() Field::make('color')->hexColor()从源码看,这些方法大多是对rule()的薄封装:activeUrl()内部执行$this->rule('active_url', $condition),alpha()执行$this->rule('alpha', $condition),以此类推。每个方法都接受一个bool | Closure $condition = true参数,意味着你可以传闭包实现条件化校验(例如仅当满足某条件时才启用该规则)。
2.2 日期比较类规则
| 方法 | 校验语义 |
|---|---|
after($date) | 字段值必须是给定日期之后的日期 |
afterOrEqual($date) | 字段值必须是大于或等于给定日期的日期 |
before($date) | 字段值必须是给定日期之前的日期 |
beforeOrEqual($date) | 字段值必须是小于或等于给定日期的日期 |
日期参数支持两类取值:字符串日期(会被strtotime()解析,如'tomorrow'、'first day of next month')或另一个字段的名称(用于字段间日期比较):
// 与具体日期比较 Field::make('start_date')->after('tomorrow') Field::make('start_date')->afterOrEqual('tomorrow') Field::make('start_date')->before('first day of next month') Field::make('start_date')->beforeOrEqual('end of this month') // 与另一字段比较 Field::make('start_date') Field::make('end_date')->after('start_date') Field::make('start_date') Field::make('end_date')->afterOrEqual('start_date') Field::make('start_date')->before('end_date') Field::make('end_date') Field::make('start_date')->beforeOrEqual('end_date') Field::make('end_date')源码实现要点:dateComparisonRule()会先strtotime($date)判断传入的是否为可解析日期;若不是日期且未设置isStatePathAbsolute,则通过resolveRelativeStatePath()将其解析为同表单内其他字段的相对 state 路径,最终编译成after:end_date这样的 Laravel 规则字符串。
2.3 字段间比较类规则
| 方法 | 校验语义 |
|---|---|
confirmed() | 字段必须存在匹配的{field}_confirmation字段 |
different($field) | 字段值必须与另一个字段不同 |
same($field) | 字段值必须与另一个字段相同 |
gt($field) | 字段值必须大于另一个字段 |
gte($field) | 字段值必须大于或等于另一个字段 |
lt($field) | 字段值必须小于另一个字段 |
lte($field) | 字段值必须小于或等于另一个字段 |
// 密码确认:需要一个名为 password_confirmation 的字段 Field::make('password')->confirmed() Field::make('password_confirmation') // 备份邮箱不得与主邮箱相同 Field::make('backup_email')->different('email') // 密码确认 Field::make('password')->same('passwordConfirmation') // 数值大小比较 Field::make('newNumber')->gt('oldNumber') Field::make('newNumber')->gte('oldNumber') Field::make('newNumber')->lt('oldNumber') Field::make('newNumber')->lte('oldNumber')这些方法在源码中统一走fieldComparisonRule():将传入的字段名解析为相对 state 路径后编译成gt:oldNumber形式。比较规则的实现同时支持multiFieldComparisonRule()(多字段)与multiFieldValueComparisonRule()(字段+值,如required_if),其中prohibitedIf、prohibitedUnless、requiredIf、requiredUnless等均属后者,并且会自动把BackedEnum转换为->value标量后再拼进规则字符串。
2.4 集合与枚举类规则
| 方法 | 校验语义 |
|---|---|
in($values) | 字段必须包含在给定值列表中 |
notIn($values) | 字段必须不在给定值列表中 |
enum(EnumClass::class) | 字段必须是给定枚举类的合法值 |
startsWith($values) | 字段必须以给定的某个值开头 |
endsWith($values) | 字段必须以给定的某个值结尾 |
doesntStartWith($values) | 字段不得以给定的某个值开头 |
doesntEndWith($values) | 字段不得以给定的某个值结尾 |
Field::make('status')->in(['pending', 'completed']) Field::make('status')->notIn(['cancelled', 'rejected']) Field::make('status')->enum(MyStatus::class) Field::make('name')->startsWith(['a']) Field::make('name')->endsWith(['bot']) Field::make('name')->doesntStartWith(['admin']) Field::make('name')->doesntEndWith(['admin'])提示:toggle buttons、checkbox list、radio 与 select 字段会根据自身可用选项自动应用
in()规则,无需手动添加。源码中getInValidationRule()会在存在inValidationRuleValues时返回Rule::in($values),否则在设置了枚举时返回Rule::enum($enum)(见 CanBeValidated.php)。此外mutateStateForValidation()会把枚举对象转为标量值,以满足 Laravelin规则对标量入参的要求。
2.5 必填、可选与禁止类规则
| 方法 | 校验语义 |
|---|---|
required() | 字段值不得为空 |
nullable() | 字段值可以为空(未加required时默认如此) |
filled() | 字段存在时不得为空 |
prohibited() | 字段值必须为空 |
prohibitedIf($field, $values) | 仅当另一字段为给定值时,本字段必须为空 |
prohibitedUnless($field, $values) | 除非另一字段为给定值,否则本字段必须为空 |
prohibits($fields) | 若本字段非空,则所有其他指定字段必须为空 |
requiredIf($field, $values) | 仅当另一字段为给定值时,本字段不得为空 |
requiredIfAccepted($field) | 仅当另一字段等于 yes、on、1、"1"、true 或 "true" 时,本字段不得为空 |
requiredUnless($field, $values) | 除非另一字段为给定值,否则本字段不得为空 |
requiredWith($fields) | 仅当任一其他指定字段非空时,本字段不得为空 |
requiredWithAll($fields) | 仅当所有其他指定字段均非空时,本字段不得为空 |
requiredWithout($fields) | 仅当任一其他指定字段为空时,本字段不得为空 |
requiredWithoutAll($fields) | 仅当所有其他指定字段均为空时,本字段不得为空 |
Field::make('name')->required() Field::make('name')->nullable() Field::make('name')->filled() Field::make('name')->prohibited() Field::make('name')->prohibitedIf('field', 'value') Field::make('name')->prohibitedUnless('field', 'value') Field::make('name')->prohibits('field') Field::make('name')->prohibits(['field', 'another_field']) Field::make('name')->requiredIf('field', 'value') Field::make('name')->requiredIfAccepted('field') Field::make('name')->requiredUnless('field', 'value') Field::make('name')->requiredWith('field,another_field') Field::make('name')->requiredWithAll('field,another_field') Field::make('name')->requiredWithout('field,another_field') Field::make('name')->requiredWithoutAll('field,another_field')源码细节:required()并不直接产生规则字符串,而是设置isRequired标志;真正编译规则时,getRequiredValidationRule()依据isRequired()返回'required'或'nullable'——这正是"未标记 required 的字段默认可空"的底层实现(CanBeValidated.php)。requiredIf等条件规则由multiFieldValueComparisonRule()编译为required_if:field,value形式,支持传BackedEnum值(自动取->value)。
标记字段为必填(Marking a field as required)
默认情况下,必填字段的标签旁会显示星号*。在"所有字段都必填"的表单上你可能想隐藏星号;反之,对非必填字段也可以手动显示星号来强调。markAsRequired()正是用于控制视觉标记(注意:它本身不添加任何验证规则):
use Filament\Forms\Components\TextInput; TextInput::make('name') ->required() // 添加"必填"验证 ->markAsRequired(false); // 移除标签旁的星号 // 字段并非 required(),但仍想显示星号: TextInput::make('name') ->markAsRequired();对应实现位于 packages/forms/src/Components/Concerns/CanBeMarkedAsRequired.php:isMarkedAsRequired()在未显式设置时回退到isRequired()的结果,即星号默认跟随必填状态。
2.6 数据库存在性校验:exists 与 scopedExists
exists()用于校验字段值必须存在于数据库中。默认情况下,若表单已关联 Eloquent 模型,则直接搜索该模型对应的表(关于如何为表单设置模型,见 packages/forms/docs/02-form.md);你也可以指定自定义的表名或模型、列名:
use App\Models\Invitation; // 默认使用表单关联模型的表 Field::make('invitation')->exists() // 指定模型/表 Field::make('invitation')->exists(table: Invitation::class) // 指定列 Field::make('invitation')->exists(column: 'id') // 通过 modifyRuleUsing 进一步定制规则(此处为闭包注入) use Illuminate\Validation\Rules\Exists; Field::make('invitation') ->exists(modifyRuleUsing: function (Exists $rule) { return $rule->where('is_active', 1); })关键限制(务必理解):Laravel 原生的exists规则不会通过 Eloquent 模型查询数据库,因此:
- 不会应用模型上定义的任何全局作用域(包括软删除作用域)——即使存在同值的软删除记录,校验也会通过;
- Filament 的多租户功能同样不会默认将查询限定到当前租户。
若希望校验遵守模型的全局作用域(包括软删除与多租户),请改用scopedExists()——它用基于模型的查询替换 Laravel 原生exists实现:
use Filament\Forms\Components\TextInput; TextInput::make('email') ->scopedExists()如需修改用于存在性检查的 Eloquent 查询(例如移除某个全局作用域),通过modifyQueryUsing传入函数:
use Filament\Forms\Components\TextInput; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; TextInput::make('email') ->scopedExists(modifyQueryUsing: function (Builder $query) { return $query->withoutGlobalScope(SoftDeletingScope::class); })源码印证:scopedExists()会基于$component->getModel()发起$model::query()->where($column, $value)查询,并默认withoutGlobalScope(SoftDeletingScope::class),再通过modifyQueryUsing钩子允许你移除或追加作用域(CanBeValidated.php)。失败时使用validation.exists语言包消息(可被validationMessages()覆盖)。
2.7 数据库唯一性校验:unique 与 scopedUnique
unique()用于校验字段值在数据库中必须唯一:
Field::make('email')->unique()如果 Filament 表单已经关联了 Eloquent 模型(例如在 panel 资源中,见 docs/03-resources/01-overview.md),Filament 会自动使用该模型。也可以显式指定表/模型与列:
use App\Models\User; Field::make('email')->unique(table: User::class) Field::make('email')->unique(column: 'email_address')忽略当前记录(更新场景的关键)
在"编辑资料"表单中(包含姓名、邮箱、所在地),通常仍要校验邮箱唯一——但如果用户只改了姓名、没动邮箱,就不该因为邮箱属于本人而报错。只要表单已关联 Eloquent 模型(如 panel 资源),Filament 默认就会忽略当前记录。可用参数控制:
// 禁止 Filament 自动忽略当前 Eloquent 记录 Field::make('email')->unique(ignoreRecord: false) // 指定忽略某条 Eloquent 记录 Field::make('email')->unique(ignorable: $ignoredUser) // 通过 modifyRuleUsing 进一步定制 use Illuminate\Validation\Rules\Unique; Field::make('email') ->unique(modifyRuleUsing: function (Unique $rule) { return $rule->where('is_active', 1); })源码中ignoreRecord的默认值来自shouldUniqueValidationIgnoreRecordByDefault()(默认true),配合ignorable时通过Rule::unique(...)->ignore($record->getOriginal($key), $record->getQualifiedKeyName())构造忽略逻辑(CanBeValidated.php)。
同样的全局作用域限制:Laravel 原生unique规则不会经过 Eloquent 模型,因此不会应用全局作用域(含软删除与多租户)。这可能导致"同值软删除记录也会导致校验失败"。解决方案是scopedUnique():
use Filament\Forms\Components\TextInput; TextInput::make('email') ->scopedUnique()同样支持忽略当前记录与修改查询:
use Filament\Forms\Components\TextInput; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\SoftDeletingScope; TextInput::make('email') ->scopedUnique(modifyQueryUsing: function (Builder $query) { return $query->withoutGlobalScope(SoftDeletingScope::class); })从源码看,scopedUnique()基于模型查询$model::query()->where($column, $value),并通过whereKeyNot($ignorable)排除当前记录,然后交给modifyQueryUsing进一步调整(CanBeValidated.php)。
三、其他 Laravel 规则:rules() 方法
任何 Laravel 原生验证规则都可以通过rules()方法附加到字段上,支持数组或|分隔的字符串两种形式:
TextInput::make('slug')->rules(['alpha_dash']) // 等价写法 TextInput::make('slug')->rules('alpha_dash|max:255')从源码看,rules()内部会将字符串按|拆分为数组,每条规则连同条件一起存入$this->rules;它还接受闭包作为规则来源,以及bool | Closure $condition条件参数,实现规则的条件启停(CanBeValidated.php)。完整规则清单可查阅 Laravel 官方验证文档。
四、自定义规则(Custom Rules)
Filament 完全兼容 Laravel 的自定义验证规则:
// 规则类对象 TextInput::make('slug')->rules([new Uppercase()]) // 闭包规则 use Closure; TextInput::make('slug')->rules([ fn (): Closure => function (string $attribute, $value, Closure $fail) { if ($value === 'foo') { $fail('The :attribute is invalid.'); } }, ])在自定义规则中注入其他字段状态
如果你的自定义规则需要引用表单中其他字段的值,可以利用 Filament 的字段工具注入(utility injection,见 packages/forms/docs/01-overview.md),把$get等工具注入到闭包规则中。方法是将闭包规则再包一层函数:
use Filament\Schemas\Components\Utilities\Get; TextInput::make('slug')->rules([ fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) { if ($get('other_field') === 'foo' && $value !== 'bar') { $fail("The {$attribute} is invalid."); } }, ])外层函数让 Filament 得以解析并注入Get工具,内层闭包再通过use ($get)捕获它——这是引用同表单其他字段状态的标准模式。
五、自定义校验属性名(validationAttribute)
字段校验失败时,错误消息中使用的属性名默认取自字段的 label。可用validationAttribute()自定义:
use Filament\Forms\Components\TextInput; TextInput::make('name') ->validationAttribute('full name')除了静态字符串,该方法也接受一个函数来动态计算属性名(可注入各种工具)。源码中getValidationAttribute()在显式设置时返回其求值结果,否则回退到Str::lcfirst($label)(即标签首字母小写)(CanBeValidated.php)。
六、定制验证错误消息(validationMessages)
默认使用 Laravel 的标准错误消息。通过validationMessages()按规则名覆盖:
use Filament\Forms\Components\TextInput; TextInput::make('email') ->unique(/* ... */) ->validationMessages([ 'unique' => 'The :attribute has already been registered.', ])每条消息既可以写静态字符串,也可以用函数动态计算(可注入工具)。消息在getValidationMessages()中被逐条求值(CanBeValidated.php),并最终通过dehydrateValidationMessages()以{statePath}.{rule}为键合入整表单的消息表。
允许在验证消息中渲染 HTML
出于 XSS 防护,验证消息默认以纯文本渲染。某些场景(如展示列表或链接)需要渲染 HTML,可显式开启:
use Filament\Forms\Components\TextInput; TextInput::make('password') ->required() ->rules([ new CustomRule(), // 返回包含 HTML 的验证消息的自定义规则 ]) ->allowHtmlValidationMessages()⚠️ 危险操作提示:开启该选项等于对该字段的验证消息放弃转义。请确保每一条消息——包括来自自定义规则或翻译文件的——都足够安全。不可信内容可能导致 XSS。源码中
allowHtmlValidationMessages()的注释也明确记录了这一点(CanBeValidated.php)。
七、禁用未保存字段的校验(validatedWhenNotDehydrated)
默认情况下,即使字段设置了"不保存"(saved(false)/dehydrated(false),见 packages/forms/docs/01-overview.md),它仍然参与校验。如果希望未保存的字段不再校验,使用validatedWhenNotDehydrated(false):
use Filament\Forms\Components\TextInput; TextInput::make('name') ->required() ->saved(false) ->validatedWhenNotDehydrated(false)该方法同样支持传函数动态计算。对应实现位于 packages/schemas/src/Components/Concerns/HasState.php,而规则收集阶段的过滤逻辑在 Schema 层:getValidationRules()会跳过isNeitherDehydratedNorValidated()的组件(packages/schemas/src/Concerns/CanBeValidated.php)。
八、验证规则的底层流水线:从字段到 Validator
了解规则如何从字段方法变成 Laravel Validator 的输入,有助于调试复杂表单:
- 收集:Schema 的
getValidationRules()遍历所有组件(含隐藏组件),对每个实现HasValidationRules契约的组件调用dehydrateValidationRules($rules),以state 路径为键写入规则表(CanBeValidated.php)。 - 编译:字段的
getValidationRules()将"必填/可空"规则、长度规则(若字段实现CanBeLengthConstrained)、in/enum规则、正则规则以及所有通过rule()/rules()追加的规则汇总为数组(CanBeValidated.php)。条件为闭包时逐条求值,不满足条件的规则被跳过。 - 执行:Schema 的
validate()最终调用$livewire->validate($rules, $messages, $attributes)(packages/schemas/src/Concerns/CanBeValidated.php),走 Laravel 标准验证流程——同时为 Livewire 前端实时校验与后端提交校验复用同一套规则。 - 测试佐证:仓库测试 tests/src/Forms/ValidationTest.php 覆盖了必填规则触发(
->required()产生Required失败键)、自定义规则透传(->rule('email'))、条件校验(->required($bool)的随机开关)以及"未脱水字段默认仍校验 / 配置后不再校验"等行为,可作为自定义验证逻辑的行为参照。
九、实战建议小结
- 优先专用方法:
->required()、->email()这类方法语义清晰、自动处理 attribute 与 state 路径,尽量避免手写rules(['required', ...])。 - 记住默认可空:未加
required的字段默认注入nullable,空值不会触发格式类规则报错。 - 区分原生与 scoped 数据库规则:涉及软删除模型或多租户表单时,使用
scopedUnique()/scopedExists()才能让全局作用域生效。 - 更新表单务必忽略自身记录:
unique默认忽略当前关联记录,跨场景显式传ignorable或ignoreRecord更稳妥。 - HTML 消息默认关闭:除非消息内容完全可信,否则不要开启
allowHtmlValidationMessages()。 - 未保存字段默认仍校验:需要跳过时显式
validatedWhenNotDehydrated(false)。
通过组合上述规则、字段工具注入与条件闭包,你可以为任何 Filament 表单构建从客户端实时反馈到服务端兜底的完整验证体系。
【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps & admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filament
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考