跳转到主要内容
要从分发的扩展包中提供像 acme_tracking:AC 这样的字符串规则,需要将验证逻辑注册到验证的 Factory 中。这样,使用方应用的表单请求就可以用与内置规则相同的写法来使用它。 本页基于 Laravel Framework v13.40.0 的官方实现。它补充了自定义验证规则中的基础示例,讨论在将其作为扩展包的公开 API 维护时的注册、翻译以及状态的生命周期。

与规则对象区分使用

官方文档介绍的 ValidationRule 对象同样可以在扩展包中使用。如果”能够以字符串形式指定”本身并不是需求,可以考虑使用能在构造函数中接收类型化参数的对象。 即使方法名同样是 validate(),调用约定也不同。如果将 ValidationRule 的实现类直接作为类字符串传给 Factory::extend(),第 3 个参数的类型和返回值都不匹配。

区分注册、创建和验证

Validator 门面的 extend() 是对 Illuminate\Validation\Factory 的调用。它与验证输入的 Illuminate\Validation\Validator 实例是不同的对象。 上图展示的是字符串规则被执行时的路径。因未填写或 nullable 等原因而跳过的情况将在后文说明。 Factory 以 validator 为键注册为 singleton,但标准实现的 make() 每次返回的都是新创建的 Validator。之后再向 Factory 注册的内容,不会更新已创建的 Validator 的扩展数组。

注册扩展包专属的规则

作为示例,我们验证一种由大写英文字母前缀、连字符和 8 位数字组成的虚构追踪码。AC-00000001 只是格式示例,并非真实快递公司的单号规范。 假设扩展包的命名空间 Acme\Courier\ 通过 Composer 的 PSR-4 映射到 src/,并且作为普通的非延迟 Service Provider 加载。关于 Provider 的加载,请参阅扩展包自动发现。

将验证逻辑拆分到类中

在 src/Validation/TrackingCode.php 中准备一个用于字符串规则的处理类。该类不实现 ValidationRule 接口。
回调会收到 4 个参数:属性名、输入值、参数数组以及正在执行验证的 Validator。对于 acme_tracking:AC,参数数组为 ['AC']。规则内部也会检查值的类型,这样即使单独使用该规则也不会产生类型错误。 不合法的前缀或参数数量不是用户的输入错误,而是被视为规则定义的配置错误并抛出异常。当规则因空值而被跳过时,这一配置检查也不会执行,因此还需要使用合法的非空值进行测试。

在 Factory 解析后注册

在 src/CourierServiceProvider.php 的 boot() 中注册规则和消息替换处理。
callAfterResolving() 在 Factory 尚未解析时会等待其创建,而对于已经解析的 Factory 则会立即注册。关于该钩子本身的详细信息,请参阅扩展包的服务解析后钩子。 类字符串 TrackingCode::class.'@validate' 会在验证时通过容器解析该类并调用方法。即使省略 @validate 只写类名,默认的方法名也是 validate。如果存在构造函数依赖,请进行绑定,确保容器能够解析。 本示例没有以 singleton 方式注册处理类,也没有设置保存输入的属性。验证时的数据通过参数处理,设计上不要把请求特有的状态捕获进 Factory 所持有的闭包中。

在使用方应用中指定

bail 会在该属性的前置规则失败时停止后续规则。必填性交给 required,格式交给 acme_tracking,以此划分职责。 公开的名称统一使用像 acme_tracking 这样独有的 snake case。对同一类扩展以相同名称注册时,后注册的会覆盖之前的。另外,如果存在内置的 validateRequired() 等方法,会直接调用该方法,因此无法用同名扩展替换内置验证。

保留用户的错误消息

普通字符串规则的消息大致按以下优先级选择。尺寸规则等特殊分支不在此讨论范围内。 Laravel 替换 :attribute 等占位符之后,才会调用自定义 replacer。replacer 的参数有 5 个:已选择的消息、属性名、规则名、参数数组和 Validator。前面的示例省略了未使用的第 5 个参数。
如果 replacer 总是把整条消息替换为固定文本,就会忽略用户的 messages() 和翻译覆盖。前面的示例只替换传入的 $message 中的 :prefix。
要在应用侧指定日语的默认文本,可以在现有 lang/ja/validation.php 的返回数组中添加 acme_tracking。下面的示例是要添加的条目示例,并不会删除其他内置消息。
关于翻译文件的准备和区域设置,请参阅本地化。如果在注册时执行 __('...') 并保存为 fallback,Factory 中保留的将是当时区域设置下的字符串。在会切换区域设置的应用中,应使用生成错误时的翻译。

使用扩展包的命名空间翻译时

在 boot() 中调用 loadTranslationsFrom(__DIR__.'/../lang', 'acme-courier') 后,就可以通过 acme-courier::validation.tracking_code 引用扩展包中的 lang/ja/validation.php 等文件。 但是,仅将翻译键传给 extend() 的第 3 个参数并不会自动翻译。 Factory 只会把它当作普通的 fallback 字符串保存。将前面钩子中的 extend() 和 replacer() 替换为下面的注册方式后,只有在选中 fallback 时才会获取扩展包的翻译。
在扩展包各区域设置的 validation.php 中准备 tracking_code 条目。日语文本可以与前面的 acme_tracking 条目相同。 这里由于在 replacer 中获取了新的翻译字符串,因此也显式地把 :attribute 传给了翻译。Laravel 的标准替换不会再次应用于这个返回值。如果选中的是用户的自定义消息,则保持该文本不变。关于分发翻译的覆盖,请参阅扩展包翻译的覆盖与更新。

理解 Implicit 与 nullable 的执行条件

extendImplicit() 注册的规则,即使属性缺失或为空字符串也会成为验证候选。它并不意味着”一定失败”或”忽略所有其他跳过条件”。 上表比较的是各规则的执行条件。因 bail、属性排除、stopOnFirstFailure() 等原因而提前停止处理的情况另当别论。在 HTTP 输入中,空字符串可能会被中间件转换为 null,因此请分别测试空字符串和 null。 本例中的格式规则,使用普通的 extend() 与 required 组合就足够了。将已公开的规则改为 Implicit 后,以前不会被验证的输入可能会失败,因此应将其视为兼容性变更。

使用 Dependent 规则解析数组中的引用目标

对于以另一个字段名作为参数的规则,可以使用 extendDependent()。在验证 items.*.confirmed_tracking 时,参数中的 items.*.tracking 会被转换为同一行的 items.0.tracking 等。普通的 extend() 不会进行这种替换。 下面是用于确认这一机制的匹配规则。如果只是验证简单的相等,实际应用中可以使用内置的 same。请将其视为应用于扩展包特有的跨字段验证时的示例。 将其添加到从钩子中接收到的 Factory。
注册为 Dependent 并不能保证引用目标存在或已通过验证。第 4 个参数的 getData() 返回的是验证对象的数据,而不仅仅是已验证的数据。请为引用方和被引用方都定义所需的规则。 此外,注册为 Dependent 并不会使其成为 Implicit。针对缺失或空值的必填性需要另行指定。由于实现会对参数进行点号转义处理和 * 替换,因此应避免设计在字段名之外混入任意字符串的 API。

在应用中测试从注册到验证的全过程

在加载了扩展包和 Service Provider 的 Laravel 13 应用中,创建 tests/Feature/TrackingRuleTest.php。除了类的单元测试外,还要通过实际的 Factory 验证注册和消息处理。
最后一个测试不修改共享的 Factory,而是新建一个 Factory 来确认注册内容的复制行为。如果还提供命名空间翻译或 Dependent 的示例,请在包含相应注册的环境中准备额外的测试。

长期维护时需要确认的事项

  • 将规则名、参数的含义与数量、必填性以及可接受的值类型作为公开 API 记录下来。
  • 验证不合法的参数,以及缺失、空字符串、null 与 nullable、sometimes、bail 的各种组合。
  • 确认用户的 messages()、属性显示名、区域设置切换以及扩展包翻译的覆盖。
  • 对于 Dependent 规则,使用多行和嵌套数组确认不会引用其他行的值。
  • 确认向未解析和已解析的 Factory 注册的行为,不要期望注册内容会追溯反映到已创建的 Validator。
  • 避免与多个扩展包发生名称冲突,不要将同一名称混用于普通、Implicit 和 Dependent 注册。
  • 将变更反映到长生命周期 worker 时要重启进程,不要在共享的处理类中残留输入或用户信息。
关于扩展包独立的测试环境,请参阅扩展包测试;关于兼容性的处理,也请参阅扩展包的版本兼容性管理。

参考的一手资料

最后修改于 2026年11月5日