☰
Respect Validation 的 EndsWith 验证器:字符串与数组的尾部匹配实现与多值支持
2026/10/6 7:51:50 网站建设 项目流程
  • 后端
  • 开发工具

【免费下载链接】Validation

The most awesome validation engine ever created for PHP

项目地址:https://gitcode.com/gh_mirrors/va/Validation
点击查看免费下载

导读

EndsWith是 Respect Validation 库中用于校验输入是否以指定值结尾的核心验证器,覆盖字符串与数组两种输入类型。本文以其官方文档 docs/validators/EndsWith.md 为骨架,结合 src/Validators/EndsWith.php 的源码实现与 tests/unit/Validators/EndsWithTest.php、tests/feature/Validators/EndsWithTest.php 的测试用例,完整讲解其构造函数签名、字符串/数组匹配语义、多值匹配、消息模板占位符以及链式 API 变体。读完本文,你将能准确使用v::endsWith()完成后缀校验,并理解其内部类型守卫与多字节安全实现原理。

功能定位:与Contains同族,但只校验结尾

EndsWith在验证器家族中与Contains属于同一类"包含性匹配"工具。官方文档明确说明:该验证器与Contains()类似,但只校验其中一个值是否出现在输入的最末尾。二者在 docs/validators/Contains.md 与 docs/validators/EndsWith.md 中互为 See Also,且同样归类于 Arrays(数组)与 Strings(字符串)两大类别。

一个直观的区分:Contains校验"字符串中是否包含某值",EndsWith则进一步限定"该值必须落在字符串的尾部"或"数组的最后一个元素"。例如输入'lorem ipsum'中既包含也以'ipsum'结尾,两者都能通过;但输入'ipsum lorem'只包含'ipsum'而不以其结尾,Contains通过而EndsWith失败。

构造函数签名与基本用法

构造函数支持两种形态(对应文档首部的 API 说明):

EndsWith(mixed $endValue) EndsWith(mixed $endValue, mixed ...$endValues)
  • 单值形态:只校验一个结尾值;
  • 多值形态(自 3.1.0 起):通过可变参数...$endValues传入多个候选值,任一匹配即通过(or 语义)。

在源码中,两种形态统一收敛为:

public function __construct(mixed $endValue, mixed ...$endValues) { $this->endValues = [$endValue, ...$endValues]; }

可见 src/Validators/EndsWith.php 将第一个参数与后续可变参数合并为内部数组$endValues,并保证其非空(@var non-empty-array<mixed>)。

字符串用法示例

官方文档给出的基础示例:

v::endsWith('ipsum')->assert('lorem ipsum'); // Validation passes successfully v::endsWith(', PhD', ', doctor')->assert('Jane Doe, PhD'); // Validation passes successfully

第二个示例展示了多值形态:只要'Jane Doe, PhD'以', PhD'或', doctor'中任意一个结尾即通过,这里命中前者。注意多值形态下即使传入', doctor'这样的"干扰项"也不会导致失败。

数组用法示例

v::endsWith('ipsum')->assert(['lorem', 'ipsum']); // Validation passes successfully v::endsWith('.', ';')->assert(['this', 'is', 'a', 'tokenized', 'phrase', '.']); // Validation passes successfully v::endsWith('.', ';')->assert(['this', 'is', 'a', 'tokenized', 'phrase']); // → `["this", "is", "a", "tokenized", "phrase"]` must end with "." or ";"

对数组而言,EndsWith只关心最后一个元素(即end($input)),中间的任意元素一概忽略。第三个示例中数组末尾元素是'phrase',既不是'.'也不是';',因此抛出校验异常,错误消息(含{{endValues|list:or}}列表渲染)随之生成。

源码级匹配语义剖析

evaluate()是验证器的核心入口,负责选择模板并生成 Result:

public function evaluate(mixed $input): Result { $template = self::TEMPLATE_STANDARD; $parameters = [ 'endValue' => $this->endValues[0], 'endValues' => $this->endValues, ]; if (count($this->endValues) > 1) { $template = self::TEMPLATE_MULTIPLE_VALUES; } return Result::of($this->validateIdentical($input), $input, $this, $parameters, $template); }

模板的选择逻辑很清晰:只有一个候选值时用TEMPLATE_STANDARD(消息只含{{endValue}}),有多个候选值时自动切换为TEMPLATE_MULTIPLE_VALUES(消息用{{endValues|list:or}}渲染成 "A or B" 列表)。

实际的匹配逻辑位于私有方法validateIdentical(),其行为可归纳为三条规则:

规则一:数组输入只看末元素,且用严格比较。

if (is_array($input) && end($input) === $endValue) { return true; }

end($input) === $endValue是严格全等比较(===),不进行类型强制转换。这一点被单元测试精准锁定:

  • [new EndsWith(1), [2, 3, 1]]判定为有效(整数1与末元素1全等);
  • [new EndsWith('1'), [2, 3, 1]]判定为无效(字符串'1'与整数1不全等);
  • [new EndsWith('1'), [2, 3, '1']]判定为有效(字符串'1'与字符串'1'全等)。

规则二:字符串输入使用多字节安全的位置判断。

if ( is_string($input) && is_string($endValue) && mb_strrpos($input, $endValue) === mb_strlen($input) - mb_strlen($endValue) ) { return true; }

实现技巧值得注意:它并非"先查末尾子串再比对",而是借助mb_strrpos(多字节安全的最后一次出现位置)与长度差做数学判断——当$endValue最后一次出现在$input中的位置恰等于strlen($input) - strlen($endValue)时,$endValue必然是$input的后缀。所有mb_*函数确保了对 UTF-8 等多字节字符集的安全处理,避免按字节切割导致乱码误判。

规则三:任一候选值命中即返回true(or 语义)。

外层foreach ($this->endValues as $endValue)遍历全部候选值,只要有一个命中就提前返回,否则全部遍历完返回false。

大小写敏感:3.0.0 起的行为变更

Changelog 中 3.0.0 一栏写明 "Case-insensitive comparison removed"(移除了大小写不敏感比较)。也就是说,当前版本下EndsWith是严格大小写敏感的。单元测试用反例锁定该行为:

[new EndsWith('foo'), 'barbazFOO'], // 'FOO' 大写,无效 [new EndsWith('foo'), 'barfaabaz'], // 位置不对,无效 [new EndsWith('foo'), 'faabarbaz'], // 结尾不符,无效

如果需要大小写不敏感的结尾匹配,不应指望EndsWith本身,而应组合其他手段(例如配合Regex使用i修饰符,见 docs/validators/Regex.md),或使用Lowercase先归一化输入。这一点是迁移自 v2 用户需要特别留意的破坏性变更。

内部类型守卫:非字符串输入安全失败

文档强调:"Only string inputs and string end values are checked; non‑string values are considered invalid but will not produce PHP errors thanks to internal type guards."

这正是validateIdentical()中两个is_string()守卫的作用。以mb_strrpos为例,若直接对非字符串输入调用会导致 PHP 警告甚至 TypeError,而源码通过类型检查将非字符串路径静默导向false(校验失败),从而把"崩溃"转化为"可预期的校验失败"。

feature 测试显式覆盖了这一边界场景:

// ensure non-string values do not throw errors and are considered invalid test('non-string input or end value are invalid', function (): void { expect(fn() => v::endsWith('foo')->assert(123)) ->toThrow(ValidationException::class); expect(fn() => v::endsWith(123)->assert('foo')) ->toThrow(ValidationException::class); });

assert(123)会抛出 ValidationException(校验失败),但不会产生 "mb_strrpos(): Argument #1 ($haystack) must be of type string" 一类的 PHP 警告或 TypeError。单元测试同样收录了这两个反例([new EndsWith('foo'), 123]与[new EndsWith(123), 'foo']),注释明确写着 "non-string inputs/values should not trigger warnings"。

消息模板与占位符

EndsWith通过 PHP 8 属性#[Template]声明两套消息模板,对应两种模式,官方文档完整罗列如下。

EndsWith::TEMPLATE_STANDARD(单值)

ModeTemplate
default{{subject}} must end with {{endValue}}
inverted{{subject}} must not end with {{endValue}}

EndsWith::TEMPLATE_MULTIPLE_VALUES(多值)

ModeTemplate
default{{subject}} must end with {{endValues|list:or}}
inverted{{subject}} must not end with {{endValues|list:or}}

占位符说明

PlaceholderDescription
subjectThe validated input or the custom validator name (if specified).
endValueThe value that will be checked to be at the end of the input.
endValuesAdditional values to check.

{{endValues|list:or}}中的list:or是模板渲染器(见 src/Message/ 下的 Formatter 与 InterpolationRenderer 体系)提供的列表过滤器:将endValues数组渲染为以 "or" 连接的英文列表。feature 测试的断言直接印证了该渲染结果:

test('Scenario #5', catchMessage( fn() => v::endsWith('Mr.', 'Dr.')->assert('John Doe'), fn(string $message) => expect($message)->toBe('"John Doe" must end with "Mr." or "Dr."'), )); test('Scenario #6', catchFullMessage( fn() => v::not(v::endsWith('divorced.', 'PhD.'))->assert('John Doe, PhD.'), fn(string $fullMessage) => expect($fullMessage)->toBe('- "John Doe, PhD." must not end with "divorced." or "PhD."'), ));
  • 多值 default 消息:"John Doe" must end with "Mr." or "Dr.";
  • 多值 inverted 消息:"John Doe, PhD." must not end with "divorced." or "PhD."。

另外,消息中的subject被渲染为带引号的输入值(如"bar")或 PHP 数组字面量(如`["bar", "foo"]`),这在catchMessage与catchFullMessage的多个场景(Scenario #1~#4)中均有覆盖:

// Scenario #1 v::endsWith('foo')->assert('bar'); // → '"bar" must end with "foo"' // Scenario #2 v::not(v::endsWith('foo'))->assert(['bar', 'foo']); // → '`["bar", "foo"]` must not end with "foo"'

模板本身定义在 src/Validators/EndsWith.php 顶部的属性中:

#[Template( '{{subject}} must end with {{endValue}}', '{{subject}} must not end with {{endValue}}', )] #[Template( '{{subject}} must end with {{endValues|list:or}}', '{{subject}} must not end with {{endValues|list:or}}', self::TEMPLATE_MULTIPLE_VALUES, )]

若需自定义消息,可通过库的setTemplate()/withTemplate()机制替换,详见 docs/validators/Templated.md 与 docs/messages/placeholder-conversion.md。

链式 API 与组合变体

EndsWith被接入库的 Mixins 体系(见 src/Mixins/),提供了丰富的链式入口,全部签名统一为endsWith(mixed $endValue, mixed ...$endValues):

变体定义位置作用
v::endsWith(...)src/Mixins/Builder.php静态入口,返回 Chain
->endsWith(...)src/Mixins/Chain.php链式追加
v::notEndsWith(...)/->notEndsWith(...)src/Mixins/NotBuilder.php、src/Mixins/NotChain.php取反校验
v::nullOrEndsWith(...)/->nullOrEndsWith(...)src/Mixins/NullOrBuilder.php、src/Mixins/NullOrChain.phpnull 视为通过
v::allEndsWith(...)/->allEndsWith(...)src/Mixins/AllBuilder.php、src/Mixins/AllChain.php对数组每个元素应用
v::keyEndsWith($key, ...)/->keyEndsWith($key, ...)src/Mixins/KeyBuilder.php、src/Mixins/KeyChain.php校验数组指定键
v::propertyEndsWith($prop, ...)/->propertyEndsWith($prop, ...)src/Mixins/PropertyBuilder.php、src/Mixins/PropertyChain.php校验对象指定属性
v::undefOrEndsWith(...)/->undefOrEndsWith(...)src/Mixins/UndefOrBuilder.php、src/Mixins/UndefOrChain.php未定义视为通过

例如,同时校验"以.md结尾"与"不以.bak结尾"可以链式组合:

v::endsWith('.md') ->not(v::endsWith('.bak')) ->assert('docs/validators/EndsWith.md');

feature 测试中的v::not(v::endsWith('foo'))->assert(['bar', 'foo'])就是取反变体的直接证据:数组以'foo'结尾,取反后校验失败。

与其他验证器的关系与选型

官方文档 See Also 列出五个相关验证器,它们构成一组"首尾/包含性匹配"工具集:

  • Contains:校验输入包含某值(字符串任意位置 / 数组任意元素),是EndsWith的"宽松版";
  • StartsWith:校验输入以某值开头,与EndsWith首尾对称。其源码 src/Validators/StartsWith.php 与EndsWith结构几乎镜像——数组用reset($input) === $startValue检查首元素,字符串用mb_strpos($input, $startValue) === 0判断前缀,同样支持多值...$startValues与TEMPLATE_MULTIPLE_VALUES;
  • In:校验输入是否落在给定的值集合内;
  • Regex:正则表达式匹配,适合实现大小写不敏感或更复杂的后缀模式;
  • Trimmed:校验字符串无首尾空白,常与EndsWith配合避免尾部空格导致误判。

选型建议:需要严格后缀(无论字符串还是数组末元素)且大小写敏感时用EndsWith;只需"包含"语义时用Contains;需要正则级别的后缀模式(如/\.(md|txt)$/i)时用Regex。

变更历史

官方 Changelog 记录了该验证器的演进轨迹:

VersionDescription
3.1.0Added support for multiple values
3.0.0Case-insensitive comparison removed
0.3.9Created
  • 0.3.9:EndsWith随早期版本创建;
  • 3.0.0:移除大小写不敏感比较,此后为严格大小写敏感(破坏性变更,v2 迁移用户需注意);
  • 3.1.0:新增多值支持,即EndsWith($endValue, ...$endValues)与TEMPLATE_MULTIPLE_VALUES、{{endValues|list:or}}列表消息渲染。

实战小结

EndsWith是 Respect Validation 中语义清晰、实现稳健的尾部匹配验证器。使用要点可归结为四点:

  1. 字符串:多字节安全的后缀判断,大小写敏感;
  2. 数组:严格全等(===)比较最后一个元素,类型不自动转换;
  3. 多值:...$endValues提供 or 语义,任一命中即通过,消息自动渲染为"A" or "B"列表;
  4. 类型守卫:非字符串输入或非字符串结束值一律判定为校验失败,但绝不触发 PHP 类型相关警告,可放心用于表单等不可信输入场景。

配合not、nullOr、all、key、property等 Mixins 变体,EndsWith可以在链式校验中覆盖对象属性、数组键以及批量元素等复杂场景,是文件后缀、句子收尾、序列末元素等校验需求的直接答案。

  • 后端
  • 开发工具

【免费下载链接】Validation

The most awesome validation engine ever created for PHP

项目地址:https://gitcode.com/gh_mirrors/va/Validation
点击查看免费下载
上一篇:CAJ转PDF一条命令本地搞定:开源工具caj2pdf实战指南
下一篇:Mac Mouse Fix使用指南:让普通鼠标在macOS上脱胎换骨的免费神器

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询