- 后端
- 开发工具
【免费下载链接】Validation
The most awesome validation engine ever created for PHP
导读
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(单值)
| Mode | Template |
|---|---|
default | {{subject}} must end with {{endValue}} |
inverted | {{subject}} must not end with {{endValue}} |
EndsWith::TEMPLATE_MULTIPLE_VALUES(多值)
| Mode | Template |
|---|---|
default | {{subject}} must end with {{endValues|list:or}} |
inverted | {{subject}} must not end with {{endValues|list:or}} |
占位符说明
| Placeholder | Description |
|---|---|
subject | The validated input or the custom validator name (if specified). |
endValue | The value that will be checked to be at the end of the input. |
endValues | Additional 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.php | null 视为通过 |
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 记录了该验证器的演进轨迹:
| Version | Description |
|---|---|
| 3.1.0 | Added support for multiple values |
| 3.0.0 | Case-insensitive comparison removed |
| 0.3.9 | Created |
- 0.3.9:
EndsWith随早期版本创建; - 3.0.0:移除大小写不敏感比较,此后为严格大小写敏感(破坏性变更,v2 迁移用户需注意);
- 3.1.0:新增多值支持,即
EndsWith($endValue, ...$endValues)与TEMPLATE_MULTIPLE_VALUES、{{endValues|list:or}}列表消息渲染。
实战小结
EndsWith是 Respect Validation 中语义清晰、实现稳健的尾部匹配验证器。使用要点可归结为四点:
- 字符串:多字节安全的后缀判断,大小写敏感;
- 数组:严格全等(
===)比较最后一个元素,类型不自动转换; - 多值:
...$endValues提供 or 语义,任一命中即通过,消息自动渲染为"A" or "B"列表; - 类型守卫:非字符串输入或非字符串结束值一律判定为校验失败,但绝不触发 PHP 类型相关警告,可放心用于表单等不可信输入场景。
配合not、nullOr、all、key、property等 Mixins 变体,EndsWith可以在链式校验中覆盖对象属性、数组键以及批量元素等复杂场景,是文件后缀、句子收尾、序列末元素等校验需求的直接答案。
- 后端
- 开发工具
【免费下载链接】Validation
The most awesome validation engine ever created for PHP
相关推荐
深入解析 Respect Validation 的 ContainsAny 验证器:数组与字符串的“任一包含”校验
深入解析 Respect Validation 的 ContainsAny 验证器:数组与字符串的“任一包含”校验 导读 ContainsAny 是 Respe
后端开发工具Kornia Filtering API 深度指南:用 filter2d / filter2d_separable / filter3d 自定义图像滤波算子
Kornia Filtering API 深度指南:用 filter2d / filter2d_separable / filter3d 自定义图像滤波算子 本
后端开发工具TypeScript 类型挑战 EndsWith 全解:用模板字符串类型与 infer 实现字符串结尾匹配
TypeScript 类型挑战 EndsWith 全解:用模板字符串类型与 infer 实现字符串结尾匹配 导读 本文围绕 type challenges 中等
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考