ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入解析 Respect Validation 的 KeyOptional 校验器:数组键存在才校验、缺失即通过

深入解析 Respect Validation 的 KeyOptional 校验器:数组键存在才校验、缺失即通过 后端开发工具【免费下载链接】ValidationThe most awesome validation engine ever created for PHP项目地址https://gitcode.com/gh_mirrors/va/Validation点击查看免费下载KeyOptional是 PHP 校验库 Respect Validation 中处理数组结构Structures与数组类型Arrays校验的核心校验器之一。它的核心语义只有一条当目标键存在时用指定的校验器校验该键的值当键缺失或输入根本不是数组时直接通过。这篇指南将围绕官方文档 docs/validators/KeyOptional.md 展开结合 KeyOptional 源码、相关单元测试与功能测试帮助你彻底掌握它的签名、行为边界、底层实现原理以及它与Key、KeyExists等相邻校验器的选择取舍。一、签名与核心语义KeyOptional的完整签名定义在官方文档与源码中完全一致KeyOptional(string|int $key, Validator $validator)$key数组中的目标键名既可以是字符串键也可以是整数键$validator作用于目标键值的校验器。从 源码实现 可以看到两个参数都通过构造函数以private形式持有且整个类被声明为final readonly意味着它不可被继承并且构造后不可再变更状态public function __construct( private int|string $key, private Validator $validator, ) { }它的语义可以一句话概括Validates the value of an array against a given validator when the key exists当键存在时用给定校验器校验数组对应键的值。这使它天然适合可选字段的校验场景——字段填了就严格校验没填就放行。二、基础用法四种典型输入的通过/失败行为官方文档给出了最直接的使用示例这里完整保留并逐条注释其行为v::keyOptional(name, v::stringType())-assert([]); // Validation passes successfully —— 键缺失直接通过 v::keyOptional(name, v::stringType())-assert([name The Respect Panda]); // Validation passes successfully —— 键存在且值为字符串通过 v::keyOptional(email, v::email())-assert([]); // Validation passes successfully —— 键缺失直接通过 v::keyOptional(email, v::email())-assert([email therespectpandagmail.com]); // Validation passes successfully —— 键存在且值为合法邮箱通过 v::keyOptional(age, v::intVal())-assert([age Twenty-Five]); // → .age must be an integer —— 键存在但值不是整数校验失败可见KeyOptional完整覆盖了两种关键分支输入状态行为结果键不存在跳过内部校验器通过键存在且值合法交给$validator校验通过键存在但值非法交给$validator校验失败抛出ValidationException输入不是数组/数组式对象视为找不到键通过见第四节注意事项三、自动命名与错误消息格式KeyOptional的一个显著特性是校验器的名字会被自动设置为键名因此失败消息会带上键路径前缀.让定位错误字段变得非常直观v::keyOptional(age, v::intVal())-assert([age Twenty-Five]); // → .age must be an integer功能测试 进一步印证了三种消息维度的输出结构v::keyOptional(foo, v::intType())-assert([foo string]); // message: .foo must be an integer // fullMessage: - .foo must be an integer // messages: [foo .foo must be an integer]也就是说ValidationException的messages数组以键名foo作为索引方便你按字段名精确抓取某一条失败信息而fullMessage则在每条消息前附加-作为列表前缀适合整体渲染。3.1 取反模式下的消息在not()前缀下消息会自动切换为否定形式这与Result中模式翻转的机制有关详见第五节v::not(v::keyOptional(foo, v::intType()))-assert([foo 12]); // → .foo must not be an integer而当not()遇到缺失的键时消息会呈现为KeyExists的模板v::not(v::keyOptional(foo, v::intType()))-assert([]); // → .foo must be present这一点在 功能测试 中有明确断言。它揭示了一个容易被忽略的事实KeyOptional在内部复用了KeyExists的失败模板{{subject}} must be present见 KeyExists 实现因此取反后键不存在会转译为键必须存在。3.2 自定义模板覆盖你也可以在assert()的第二个参数传入自定义模板彻底替换默认消息v::keyOptional(foo, v::intType())-assert([foo string], That key is off-key); // → That key is off-key功能测试 验证了自定义模板在取反模式下同样生效。四、重要注意事项非数组输入也会通过官方文档特别用一节 Note 强调了一个极易踩坑的行为This validator will pass for anything that is not an array because it will always pass when it doesnt find a key.KeyOptional对任何非数组输入都会直接放行因为它永远找不到键。这既是特性也是陷阱——如果你期望输入必须是一个数组就必须显式叠加ArrayTypev::arrayType()-keyOptional(phone, v::phone())-assert(This is not an array); // → This is not an array must be an array叠加后ArrayType先行兜底非数组输入会在键校验之前就被拦截。单元测试 用Stub::daze()一个永远失败的桩校验器配合任意非数组输入如整数、字符串、对象等数据提供器里的值验证了这一结论——连永远失败的校验器都无法让非数组输入报错足见这条规则是硬性的。五、底层实现原理KeyExists 与 Key 的两段式委托KeyOptional的实现非常精巧它本身并不直接读写数组而是把工作委托给两个更基础的校验器。完整逻辑在 KeyOptional::evaluate()public function evaluate(mixed $input): Result { $keyExistsResult (new KeyExists($this-key))-evaluate($input); if (!$keyExistsResult-hasPassed) { return $keyExistsResult-withNameFrom($this-validator)-withToggledModeAndValidation(); } return (new Key($this-key, $this-validator))-evaluate($input); }执行链路分两步第一步先跑KeyExists。它负责回答键是否存在。从 KeyExists 实现 看hasKey()对array使用array_key_exists()对实现了ArrayAccess的对象使用offsetExists()其余类型一律返回false——这正好解释了为什么非数组输入会被KeyOptional放行。键不存在时直接返回KeyExists的失败结果但做了两件事withNameFrom($this-validator)把名字改为内部校验器的名字若内部校验器通过named()自定义了名字则会体现在消息里见下方测试证据withToggledModeAndValidation()把hasPassed与hasInvertedMode同时翻转见 Result::withToggledModeAndValidation。这样在普通模式下键缺失仍是通过而在not()取反模式下则变成失败并呈现must be present消息——这正是第三节中取反行为能够成立的根本原因。键存在时走第二条分支构造Key($this-key, $this-validator)并把整个输入交给它由其内部取出$input[$this-key]交给内部校验器求值并附加键路径见 Key::evaluate()。单元测试 用一个记录输入值的桩校验器Stub::pass(1)验证了委托行为的正确性当键存在时内部校验器收到的输入恰好就是$input[$key]这一项而不是整个数组。5.1 为什么实现成两段式这样设计的收益是职责单一与消息复用KeyExists负责键存在性及其must be present模板Key负责取键值 子校验 路径标注KeyOptional只负责在两者之间做一次条件判断。Key、KeyExists、KeyOptional三者共享KeyRelated接口getKey(): int|string参见 Core/KeyRelated.php从源码结构上可以看出它们构成了一个内聚的键族。六、与 Key / KeyExists / PropertyOptional 的选择取舍官方文档在 Note 末尾明确指出两个最紧密的替代关系只想校验键是否存在不关心值用 KeyExists要求键必须存在、并且对值做校验用 Key键存在才校验、缺失放行用KeyOptional。三者的差异可以浓缩为下表校验器键缺失时键存在时典型用途KeyExists失败must be present通过不校验值判断必填字段是否出现Key失败must be present校验该键的值必填字段的类型/格式校验KeyOptional通过校验该键的值可选字段填了就校验从 Key 官方文档 的 Changelog 可以看出三者血缘关系3.0.0 版本中Key被拆分为KeyExists与KeyOptional而KeyOptional自身也标注为 3.0.0 从Key派生而来。此外KeyOptional还有面向对象的镜像版本PropertyOptional语义完全相同只是把数组键换成对象属性源码见 PropertyOptional.php其实现结构与KeyOptional如出一辙。如果你在对象属性场景遇到类似需求可以对照参考 PropertyOptional 文档。七、组合与链式用法KeyOptional可以被not、nullOr、undefOr等前缀组合使用组合入口在 src/Mixins 目录下均有对应生成方法例如notKeyOptional($key, $validator)—— NotBuilder / NotChainnullOrKeyOptional($key, $validator)—— NullOrBuilder / NullOrChainundefOrKeyOptional($key, $validator)—— UndefOrBuilder / UndefOrChain典型写法v::nullOrKeyOptional(nickname, v::stringType())-assert([nickname null]); // → 通过nullOr 允许 null v::notKeyOptional(age, v::intVal())-assert([age 25]); // → .age must not be an integer取反后键存在且通过内部校验即失败KeyOptional同样可以嵌套用于校验多层数组结构与Key的嵌套用法一致例如在payment_details.credit_card这样的层级中把内层字段声明为可选v::key( payment_details, v::keyOptional(credit_card, v::creditCard()) )-assert([ payment_details [ // 无 credit_card 键通过 ], ]);外层Key要求payment_details必须存在内层KeyOptional则允许credit_card缺失一旦出现就按信用卡格式严格校验。八、作为 PHP 属性Attribute声明式使用KeyOptional在源码中同时注册了 PHP 原生 Attribute支持在对象属性上做声明式校验#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class KeyOptional implements KeyRelated见 KeyOptional.php关键信息TARGET_PROPERTY可标注在类属性上IS_REPEATABLE同一属性可重复标注多个KeyOptional实现对多个键的逐一声明#[Composable(without: [All::class, Key::class, Property::class])]声明它不能与All、Key、Property互相嵌套组合避免语义冲突。该 Attribute 机制来自Respect\Fluent\Attributes意味着你可以在属性元数据驱动的校验场景中把可选字段规则直接写在属性注解里与链式 API 得到相同的校验行为。九、模板占位符与项目内其他校验器一致KeyOptional的失败消息支持一个标准占位符占位符说明subject被校验的输入或自定义的校验器名字若通过named()指定功能测试 验证了named()包装时消息的变化v::named(Wrapper, v::keyOptional(foo, v::named(Wrapped, v::intType())))-assert([foo string]); // → Wrapped must be an integer内部校验器名字被继承 v::named(Wrapper, v::keyOptional(foo, v::intType()))-assert([foo string]); // → .foo (- Wrapper) must be an integer外部包装名以 (- ...) 追加标注可以看出subject的取值优先级是内部校验器的自定义名字 键路径.foo而外层包装名会以(- Wrapper)的形式附加在消息中帮助追踪校验链来源。十、分类与版本变更分类Arrays数组、Structures结构变更记录3.0.0 版本从 Key 拆出并独立成类。十一、相关校验器速查官方文档 See Also 中给出的相关校验器统一整理如下均为仓库根目录相对路径ArrayType —— 要求输入必须是数组与KeyOptional搭配兜底ArrayVal —— 输入可被转换为数组即通过Each —— 对数组的每个元素应用校验器Key —— 要求键必须存在并对值校验KeyExists —— 只校验键是否存在Property —— 对象属性版本的KeyPropertyExists —— 对象属性版本的KeyExistsPropertyOptional —— 对象属性版本的KeyOptional小结KeyOptional是编写可选字段校验逻辑的首选工具键缺失即通过键存在则严格校验配合ArrayType可补足非数组输入的类型约束。它的两段式委托实现KeyExistsKey让存在性与取值校验两个关注点彻底解耦并通过withToggledModeAndValidation()让取反语义也保持自洽。无论是链式 API、前缀组合not/nullOr/undefOr、属性式声明还是与Key/KeyExists/PropertyOptional的搭配你都能在这一套键族校验器中找到一致且可预测的行为。赞分享后端开发工具【免费下载链接】ValidationThe most awesome validation engine ever created for PHP项目地址https://gitcode.com/gh_mirrors/va/Validation点击查看免费下载相关推荐uni-app x CSS 指南border-bottom-right-radius 右下角圆角属性全解析uni app x CSS 指南border bottom right radius 右下角圆角属性全解析 本篇技术指南围绕 uni app x 框架中 bo后端开发工具深入解析 Respect Validation 的 IntVal 校验器PHP 整数值的安全校验实践深入解析 Respect Validation 的 IntVal 校验器PHP 整数值的安全校验实践 导读 IntVal 是 Respect Validati后端开发工具Respect Validation 条件校验器 Given 深入解析当条件成立时才校验否则静默放行Respect Validation 条件校验器 Given 深入解析当条件成立时才校验否则静默放行 Given Validator $when, Vali后端开发工具上一篇Gogh项目国际化战略突破语言障碍的开源项目下一篇react-app-rewired与Firebase部署完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进