Symfony Validator 组件 CHANGELOG 全解读:从 2.1 到 8.2 的校验能力演进与实践指南

原创2026-10-03 09:29:192,246 阅读
文章标签:后端Web框架

Symfony Validator 组件 CHANGELOG 全解读:从 2.1 到 8.2 的校验能力演进与实践指南

本篇文章以 src/Symfony/Component/Validator/CHANGELOG.md 为主线,系统梳理 Symfony Validator 组件从 2.1 到 8.2 版本的演进脉络:新增了哪些约束、验证组机制如何发展、自定义约束的编写方式经历了怎样的重构,以及 8.0 带来了哪些破坏性变更。读完本文,你将能够依据版本特性选择正确的约束与配置方式,理解 GroupSequence、Valid 级联、命名参数构造器等核心机制,并能结合仓库源码验证每一项能力的真实实现。

一、Validator 组件是什么

Symfony Validator 组件用于依据 JSR-303 Bean Validation 规范对值进行校验,是 Symfony 全栈框架中表单验证、API 入参校验、领域模型约束的核心引擎。它提供三件核心能力:约束(Constraint)——描述"值必须满足什么规则";校验器(Validator)——执行规则并产出 ConstraintViolation 违规对象列表;元数据加载器(Mapping/Loader)——从属性(Attribute)、YAML、XML 配置中读取约束声明。

组件根目录包含 Constraint.php(约束基类)、ValidatorBuilder.php(构建器)、Validation.php(静态入口),以及存放全部内置约束实现的 Constraints/ 目录(截至 8.2 有 100 余个约束类)。

二、8.2:新增媒体校验与验证组级联增强

2.1 音频文件校验:Audio 约束

8.2 新增 Audio 约束,用于校验音频文件。从源码 Audio.php 可见,它继承自 File,因此天然拥有文件存在性、大小、MIME 类型、文件名长度等全部 File 能力,同时新增音频专属维度:

  • minDuration / maxDuration:最小/最大时长(秒),错误码 TOO_SHORT_ERROR / TOO_LONG_ERROR;
  • minBitrate / maxBitrate:最小/最大比特率(bps),错误码 BITRATE_TOO_LOW_ERROR / BITRATE_TOO_HIGH_ERROR;
  • allowedSampleRates:允许的采样率(Hz);
  • minChannels / maxChannels:声道数量上下限;
  • allowedCodecs / allowedContainers:允许的编码格式与容器格式;
  • 默认 mimeTypes = 'audio/*',消息默认值为 'This file is not a valid audio file.'。
use Symfony\Component\Validator\Constraints as Assert;

class PodcastUpload
{
    #[Assert\Audio(
        maxSize: '20M',
        minDuration: 30,
        maxDuration: 3600,
        allowedContainers: ['mp3', 'm4a'],
    )]
    public $file;
}

注意:该类定义了 16 个专属错误常量(如 CORRUPTED_AUDIO_ERROR、NO_AUDIO_STREAM_ERROR、MULTIPLE_AUDIO_STREAMS_ERROR 等),说明其校验逻辑依赖底层媒体解析能力,属于"尽力检测型"校验。

2.2 cron 表达式校验:Cron 约束

Cron 约束用于校验 cron 表达式,源码 Cron.php 显示它支持三种模式,通过 mode 选项选择,默认 MODE_ALIASES:

模式 常量 说明
standard Cron::MODE_STANDARD 标准五段式 cron 表达式
aliases Cron::MODE_ALIASES 额外支持 @daily、@weekly 等别名(默认)
hashed Cron::MODE_HASHED 支持哈希 cron 表达式
#[Assert\Cron(mode: 'standard')]
public string $schedule;

使用前提:该约束依赖 dragonmantank/cron-expression 包。源码在构造器中显式检查 class_exists(CronExpression::class),未安装时抛出 LogicException 并提示执行 composer require dragonmantank/cron-expression。如果 mode 不在三种模式之列,则抛出 ConstraintDefinitionException。

2.3 ValidationBundle 与配置迁移

8.2 新增 ValidationBundle(见 ValidationBundle.php),提供 validation 配置及此前由 FrameworkBundle 在 framework.validation 下提供的服务。同时抽出两个此前由 FrameworkBundle 提供的类:

这标志着校验组件的 DI 集成开始从 FrameworkBundle 中解耦,独立成可复用的 Bundle。

2.4 验证组序列的新选项:cascadeCurrentGroup

GroupSequence 与 GroupSequenceProvider 新增 cascadeCurrentGroup 选项。源码 GroupSequence.php 的注释解释了设计动机:当类声明了组序列(Default 组被序列覆盖)时,被 Valid 约束引用的对象默认只在 Default 组中级联,因此注册在序列组(如 Strict)上的约束永远不会被执行。

设置 cascadeCurrentGroup = true 后,校验器会把"当前正在执行的序列组"与 Default 组一起级联到引用对象,使这些约束得以生效:

use Symfony\Component\Validator\Constraints as Assert;

#[Assert\GroupSequence(['Basic', 'Strict'], cascadeCurrentGroup: true)]
class Order
{
    // ...
}

注意该标志只作用于"类上声明的序列";当序列由调用方显式传给 validate() 时,被级联的本来就是当前序列组,无需此选项。

同时,8.2 移除了不再使用的 GroupSequence::$cascadedGroup 属性,并允许在 GroupSequence 定义中直接书写被注解类的 ::class 常量。

2.5 Valid 约束的 restrictGroups 选项

Valid 约束新增 restrictGroups 选项(默认 true)。源码 Valid.php 中参数注释说明:为 true 时,Valid 上配置的 groups 同时限定嵌套对象使用哪些组进行校验;为 false 时,groups 只决定"何时触发级联",嵌套对象实际按当前正在校验的组执行:

#[Assert\Valid(groups: ['api'], restrictGroups: false)]
public Address $address;

配合 8.2 的另一项改动——允许在 Sequentially 中嵌套无组的 Valid 约束,实现"仅当前面约束全部通过后才执行级联校验"的效果:

#[Assert\Sequentially([
    new Assert\NotBlank(),
    new Assert\Valid(),
])]
public $payload;

2.6 Callback 约束的 message 选项与布尔回调

Callback 约束新增 message 选项(源码 Callback.php):当设置了 message 时,回调必须返回布尔值,返回 false 即自动抛出以 message 为内容的违规(错误码 NOT_SATISFIED_ERROR);不设置 message 时,返回值被忽略,回调应自行通过执行上下文构建违规:

#[Assert\Callback(
    static function (\DateTimeImmutable $value) {
        return $value > new \DateTimeImmutable('today');
    },
    message: 'The delivery date must be in the future.',
)]
public \DateTimeImmutable $deliveryDate;

2.7 其他 8.2 增强

  • ValidationFailedException 增加 #[WithHttpStatus(422)],未捕获时渲染为 "Unprocessable Content"(422)响应而非 500;
  • Range 及比较类约束支持 BcMath\Number 值;
  • ConstraintViolationBuilderInterface::setParameter() 允许传入 int、float、\Stringable、\DateTimeInterface;
  • 当 File 约束配置了 extensions 时,不再用"从扩展名自动推导的 MIME 类型"去收窄 mimeTypes 选项;
  • Unique 约束的 fields 选项支持读取对象属性(不再局限于数组键);
  • Mapping\Loader\LoaderChain::__construct() 新增可选参数 $mappedClasses,用于声明每个 loader 映射哪些类;ValidatorBuilder::addMappedClasses() 与之配套,用于声明每个映射文件映射的类,从而让"文件级元数据"拥有可检索的类清单。

三、8.1:测试性、属性元数据与校验器接口重构

3.1 属性元数据存在性检查

ValidatorBuilder::enablePropertyMetadataExistenceCheck()(ValidatorBuilder.php)启用后,validateProperty() 与 validatePropertyValue() 在目标属性没有元数据时会直接抛异常,而不是静默返回空结果——便于在类型化、强约束的代码中尽早暴露"忘了加约束"的配置错误。

3.2 findByCodes() 与时钟感知校验

  • ConstraintViolationListInterface 新增 findByCodes(),可按一个或多个错误码在违规列表中检索;
  • 比较类与 Range 校验器支持时钟注入(clock-aware),使"基于当前时间的比较校验"(如日期必须晚于今天)可以在测试中固定时间,保证可重复断言。

3.3 Xml 约束与校验器测试重构

  • 新增 Xml 约束,用于校验 XML 内容(与 7.2 的 Yaml、4.3 的 Json 形成"格式校验"家族);
  • 核心接口重构:弃用 ConstraintValidatorInterface::initialize() 与 validate(),改用 validateInContext();抽象类 ConstraintValidator.php 负责上下文管理;测试基类 ConstraintValidatorTestCase 提供新的 validate() 方法封装校验器调用方式,使编写约束校验器单元测试更简洁。

四、8.0:破坏性变更全景(升级必读)

4.1 移除 XML/YAML 隐式选项配置

8.0 彻底移除在 XML/YAML 中"隐式把 <value> 当作选项"的旧写法,必须显式声明 option name:

XML 迁移:<value> 直接书写参数 → 包裹在 <option name="callback"> 内:

<!-- Before -->
<class name="App\Entity\Order">
  <constraint name="Callback">
    <value>App\Validator\OrderValidator</value>
    <value>validate</value>
  </constraint>
</class>

<!-- After -->
<class name="App\Entity\Order">
  <constraint name="Callback">
    <option name="callback">
      <value>App\Validator\OrderValidator</value>
      <value>validate</value>
    </option>
  </constraint>
</class>

YAML 迁移:- Callback: validateMeStatic 的简写形式改为显式 callback: 键:

# Before
App\Entity\Order:
  constraints:
    - Callback: validateMeStatic
    - Callback: [App\Validator\OrderValidator, validate]

# After
App\Entity\Order:
  constraints:
    - Callback:
        callback: validateMeStatic
    - Callback:
        callback: [App\Validator\OrderValidator, validate]

(上述两种弃用自 7.4 开始,8.0 正式移除。)

4.2 约束构造器全面迁移到命名参数

这是 8.0 最重要、影响面最广的破坏性变更:基础 Constraint 类不再支持"传入选项数组再在内部求值",所有具体约束必须在构造函数中初始化属性,并将必填选项改为必填构造参数。旧的 getRequiredOptions()、getDefaultOption()、normalizeOptions() 方法被移除。

自定义简单约束迁移:

// Before (8.0 之前可用的旧写法)
class CustomConstraint extends Constraint
{
    public $option1;
    public $option2;

    public function __construct(?array $options = null)
    {
        parent::__construct($options);
    }
}

// After(8.0 推荐写法)
class CustomConstraint extends Constraint
{
    public function __construct(
        public $option1,
        public $option2 = null,
        ?array $groups = null,
        mixed $payload = null,
    ) {
        parent::__construct(null, $groups, $payload);
    }
}

必填选项迁移:不再用 getRequiredOptions() 声明必填项,而是直接写成无默认值的构造参数:

class CustomConstraint extends Constraint
{
    public function __construct(
        public $option1,        // 必填
        public $option2 = null,
        ?array $groups = null,
        mixed $payload = null,
    ) {
        parent::__construct(null, $groups, $payload);
    }
}

复合约束(Composite)迁移:不再通过 getCompositeOption() 声明嵌套选项来源,而是在子类构造器中直接初始化属性:

class CustomCompositeConstraint extends Composite
{
    public function __construct(
        public array $constraints,
        ?array $groups = null,
        mixed $payload = null,
    ) {
        parent::__construct(null, $groups, $payload);
    }
}

同时,All、AtLeastOneOf、CardScheme、Collection、CssColor、Expression、Regex、Sequentially、Type、When 等约束的 getRequiredOptions() / getDefaultOption() 被移除(7.4 弃用,8.0 移除)。

4.3 其他 8.0 变更

  • GroupSequence 不再接受关联数组:
// Before
GroupSequence(['value' => ['group 1', 'group 2']]);

// After
GroupSequence(['group 1', 'group 2']);
  • Url 约束的 $requireTld 默认值改为 true:URL 必须包含顶级域名(此变更自 7.1 引入该选项时即预告);
  • ClassMetadataInterface 新增 getGroupProvider();
  • GenericMetadata 实现由 __sleep/__wakeup() 迁移到 __(un)serialize();
  • 移除 Bic::INVALID_BANK_CODE_ERROR 常量(该校验器已不再使用);
  • 移除了基类中"求值选项"的能力,具体约束须在构造函数中初始化属性。

五、7.x:命名参数体系与格式类约束的密集扩张

5.1 7.4:为 8.0 破坏性变更铺路

7.4 集中宣布了一批弃用(弃用内容即上节 8.0 移除的各类写法),并新增两样能力:

  • #[ExtendsValidationFor]:为某个类声明"额外的"校验约束,实现跨类扩展约束而无需修改目标类;
  • ValidatorBuilder::addAttributeMappings() 与 AttributeMetadataPass:在编译期用属性声明约束元数据;
  • Video 约束:校验视频文件(与 8.2 的 Audio 同族,均继承 File 并增加媒体维度参数);
  • Length 约束违规增加 min / max 参数,便于客户端展示约束边界。

7.4 给出了自定义约束迁移到命名参数的过渡形态——配合 #<a href="https://link.gitcode.com/i/4c02017a90218355f1308cf934110fe7" target="_blank">HasNamedArguments] 属性(定义于 [Attribute/HasNamedArguments.php,作用于方法,用于向加载器提示"哪些约束选项是必需的"):

use Symfony\Component\Validator\Attribute\HasNamedArguments;

class CustomConstraint extends Constraint
{
    #[HasNamedArguments]
    public function __construct(
        public $option1,
        public $option2 = null,
        ?array $groups = null,
        mixed $payload = null,
    ) {
        parent::__construct(null, $groups, $payload);
    }
}

5.2 7.3:命名参数成为主流用法

  • 弃用"不支持命名参数的自定义约束"——新约束必须标注 #[HasNamedArguments];
  • 弃用向约束构造器传选项数组,改为每个选项一个命名参数:
// Before
new NotNull([
    'groups' => ['foo', 'bar'],
    'message' => 'a custom constraint violation message',
]);

// After
new NotNull(
    groups: ['foo', 'bar'],
    message: 'a custom constraint violation message',
);
  • Image 约束支持 SVG 的宽高比(ratio)检查;
  • When 约束支持 otherwise 选项与闭包(closures);
  • Composite 约束支持多个字段包含嵌套约束;
  • Unique 约束新增 stopOnFirstError 选项(默认 true,设为 false 时校验所有元素并报告全部违规)。

5.3 7.2~7.1:格式与标识符约束家族

7.2 新增 Yaml 约束、WordCount 约束、Week 约束;Ulid 约束新增 format 选项;Unique 新增 errorPath;WhenValidator 增加上下文变量;DateTime 违规消息新增 format 参数;IbanValidator 接受含不换行空格(non-breaking space)的 IBAN;PasswordStrengthValidator::estimateStrength() 变为 public。

7.1 新增 MacAddress 约束与 Charset 约束;Ip 约束新增 *_NO_PUBLIC、*_ONLY_PRIVATE、*_ONLY_RESERVED 版本,且 Cidr 约束可复用全部 Ip 版本;Type 约束新增 list 与 associative_array 类型;Url 新增 requireTld 选项(未传值时 7.1 开始弃用、8.0 默认 true);PasswordStrengthValidator 违规中携带计算出的强度值。

5.4 7.0:错误码常量与注解时代的终结

  • 静态属性 $errorNames 全部替换为 ERROR_NAMES 常量(同规则适用于 Ip 约束的 $versions → VERSIONS);
  • 移除 Email::VALIDATION_MODE_LOOSE,使用 VALIDATION_MODE_HTML5;
  • 移除 ExpressionLanguageSyntax 约束,使用 ExpressionSyntax;
  • 移除 Doctrine 注解支持,全面转向原生属性(Attribute):AnnotationLoader、ValidatorBuilder::enableAnnotationMapping()、setDoctrineAnnotationReader() 等全部移除,改用 AttributeLoader、enableAttributeMapping();
  • ConstraintViolationInterface 新增 getConstraint()、getCause()、__toString();ConstraintViolationBuilderInterface 新增 disableTranslation()。

六、6.x:条件校验与密码安全

6.1 6.4:When 约束的完善与 GroupProvider

  • Expression 约束的 is_valid 函数行为等同于 ValidatorInterface::validate;
  • Uuid 约束的 versions 选项允许传入单个整数;
  • When 约束的 constraints 选项允许传入单个约束;
  • 宣布注解弃用路线(与 7.0 移除对应);
  • Type 约束新增 number、finite-number、finite-float 类型;
  • Time 约束新增 withSeconds 选项;
  • 新增 GroupProviderInterface,允许在底层类之外实现校验组提供器。

6.2 6.3:密码与字符串安全

  • NoSuspiciousCharacters 约束:检测字符串是否为"仿冒攻击"(spoofing attempt);
  • PasswordStrength 约束:检查密码强度;
  • Length 约束新增 countUnit 选项:按码点(默认 Length::COUNT_CODEPOINTS)、字节(COUNT_BYTES)或字素簇(COUNT_GRAPHEMES)计数;
  • File 约束新增 filenameMaxLength;Cascade 约束新增 exclude 选项;Regex 违规新增 pattern 参数;ConstraintViolationInterface 新增 getConstraint()。

6.3 6.2~6.1:条件校验与表达式增强

6.2:新增 When 约束(满足表达式条件时才执行嵌套约束)及其校验器;Email 新增 VALIDATION_MODE_HTML5_ALLOW_NO_TLD 模式;File 新增 extensions 选项(同时校验 MIME 类型、扩展名及二者一致性);Expression 新增 negate 选项反转违规逻辑;NotCompromisedPasswordValidator 增加隐私增强的填充(padding)机制。

6.1:Unique 新增 fields 选项(只对指定集合键做唯一性校验);弃用 Constraint::$errorNames 改用 ERROR_NAMES 常量;允许使用必填参数创建约束;Choice 新增 match 选项。

6.4 6.0:清理与注解弱化

移除 Length 的 allowEmptyString 选项与 NumberConstraintTrait 特征;enableAnnotationMapping() 不再接受、也不再自动设置 Doctrine 注解读取器。

七、5.x 与 4.x:约束生态的奠基期

7.1 5.x 关键新增

  • 5.4:Cidr 约束、CssColor 约束、ConstraintViolationList::createFromMessage()、Count/Length 的 exactly 错误 UID;
  • 5.3:Unique 的 normalizer 选项、Validation::createIsValidCallable()(返回布尔而非抛异常);
  • 5.2:Cascade 约束(简化嵌套类型化属性的级联校验)、Isin、ULID、UUIDv6 支持、validator 开始支持从 PHP 属性加载约束;
  • 5.1:AtLeastOneOf(至少一个嵌套约束通过即合法)、Hostname、Country/Language 的 alpha3 选项、Compound 约束(复用约束集合)、Sequentially(顺序校验,任一违规即停止后续)、Count 的 divisibleBy、ExpressionLanguageSyntax。

7.2 5.0 破坏性变更

ExpressionValidator 构造器第一个参数必须是 ExpressionLanguage 实例或 null;移除 Url 的 checkDNS/dnsMessage、Email 的 checkMX/checkHost/strict;DateTime/Date/Time 校验器不再校验 \DateTimeInterface 实例;Bic、Country、Currency、Language、Locale 必须依赖 symfony/intl,Email 必须依赖 egulias/email-validator,Expression 必须依赖 symfony/expression-language;Locale 的 canonicalize 默认改为 true;移除 ValidatorBuilderInterface;元数据缓存切换为 PSR-6(setMappingCache)。

7.3 4.x 关键新增

  • 4.4:EnableAutoMapping / DisableAutoMapping 约束、Range 的 minPropertyPath / maxPropertyPath、TypeValidator 支持类型数组、compared_value_path 违规参数;
  • 4.3:Timezone、NotCompromisedPassword、Json、Unique、Positive/PositiveOrZero/Negative/NegativeOrZero、字符串约束的 normalizer 选项、Bic 的 iban/ibanPropertyPath、NotBlank 的 allowNull、UATP 卡支持;
  • 4.2:DivisibleBy、UnexpectedValueException(校验器抛出的异常会被转换为违规)、DateTime 的 format 默认选项;
  • 4.1:Expression 的 values 选项、弃用 Url 的 checkDNS。

八、3.x 与 2.x:组件早期奠基

3.4:Valid 约束支持校验组、Image 的 minPixels/maxPixels、比较约束的 propertyPath 选项。

3.3:AddValidatorInitializersPass、AddConstraintValidatorsPass、ContainerConstraintValidatorFactory —— 将约束校验器接入服务容器。

3.2:YAML 配置文件支持 PHP 常量;测试基类 AbstractConstraintValidatorTest 弃用,改用 Test/ConstraintValidatorTestCase.php。

3.1:DateTime 新增 format 选项。

2.8:BIC(SWIFT-Code)校验器。

2.7:弃用 DefaultTranslator;Null/True/False 及对应校验器弃用,由 IsNull/IsTrue/IsFalse 替代。

2.6:FileValidator 禁止空文件;ExpressionValidator 对 null/空字符串也执行表达式;所有约束支持通用的 payload 选项;ConstraintViolationBuilderInterface::setCause()。

2.5:大规模内部重构——引入 Mapping\MetadataInterface 体系、Context\ExecutionContextInterface、Validator\RecursiveValidator 等分层架构;GroupSequence 实现 ArrayAccess/Countable/Traversable;Callback 可作用于属性;ConstraintViolation 支持参数与复数化消息。

2.4:新增基于表达式语言的约束;Image 增加 minRatio/maxRatio/allowSquare/allowLandscape/allowPortrait。

2.3:ISBN、ISSN、IBAN 校验器;Optional/Required 约束;完整比较约束族(EqualTo、NotEqualTo、LessThan、LessThanOrEqualTo、GreaterThan、GreaterThanOrEqualTo、IdenticalTo、NotIdenticalTo)。

2.2:CardScheme、Luhn 校验器;约束/违规/执行上下文接口体系建立;违规消息支持翻译与复数化。

2.1:Count、Length、Range 约束;ctype_* 类型断言;GroupSequenceProvider 动态组序列模式;Collection 校验器的 missing/extra 错误细化;ConstraintValidatorInterface 的 isValid 更名为 validate;Validation 与 ValidatorBuilderInterface 出现。

九、实践总结:如何用好这个组件的演进成果

  1. 新项目直接用 8.x 写法:所有约束一律使用命名参数构造器,自定义约束务必标注 #<a href="https://link.gitcode.com/i/5a5e399ee28833dcfe650996acefbcfb" target="_blank">HasNamedArguments] 并将必填项写成必填构造参数,详见 [Attribute/HasNamedArguments.php;
  2. 升级 8.0 时重点排查:XML/YAML 中的隐式 <value> 配置、GroupSequence 关联数组、Url 的 requireTld 默认值、注解映射代码,四类破坏性变更均可在本 CHANGELOG 对应版本段落找到迁移示例;
  3. 按需引入依赖:Cron(dragonmantank/cron-expression)、Email(egulias/email-validator)、Expression(symfony/expression-language)、Bic/Country/Currency 等(symfony/intl)属于可选依赖,未安装时约束构造器会直接抛 LogicException,可从 composer.json 中核对建议依赖清单;
  4. 验证组是分层校验的核心工具:GroupSequence + cascadeCurrentGroup + Valid::restrictGroups 组合,可以精确控制"先做廉价校验、再做昂贵校验"的执行顺序,以及嵌套对象在各组间的级联行为;
  5. 测试优先:使用 ConstraintValidatorTestCase::validate()(8.1 起)与 CompoundConstraintTestCase(7.2 起)编写约束校验器测试,配合 enablePropertyMetadataExistenceCheck() 在测试中尽早暴露元数据缺失问题。

从 2.1 到 8.2,Validator 组件始终围绕"声明式校验"这一核心演进:约束家族不断扩充(从基础字符串/数值校验扩展到媒体文件、格式语法、密码安全、标识符),声明方式从选项数组 + 注解全面迁移到命名参数 + 原生属性,验证组与级联机制日趋精细。掌握这份 CHANGELOG 的脉络,就等于掌握了 Symfony 校验体系的设计哲学与迁移地图。

登录后查看全文
symfony