验证器
验证器提供声明式的参数校验:以 PHP 注解(Attribute)把校验规则直接标注在方法参数上,容器注入参数时自动执行校验,失败即抛出异常。校验逻辑因此与业务代码分离,规则紧贴参数定义,可读性与可维护性兼得。
本文依据框架源码
src/Core/Validate.php、src/Core/Validate/BaseValidateRule.php、src/Core/Validate/Rules/*、src/Core/Container.php(参数注入链路)、src/HttpServer/AutoInject/ValidateNull.php编写。
工作原理
容器解析方法参数时(Container::injectParams()),对每个参数依次执行:
取值(命名/位置/默认值)→ 前置注入注解 → 类型校验(Validate::check)→ 规则校验(Validate::checkRules)规则校验在容器注入参数时自动触发,无需手动调用:
use Viswoole\Core\Validate\Rules\{Length, Min, Max};
class UserController
{
public function create(
#[Length(2, 20)] string $name, // 长度 2-20(先 trim 再计算)
#[Min(18), Max(120)] int $age, // 18 ≤ age ≤ 120
): array {
// 执行到这里时,两个参数已经通过类型与规则校验
return compact('name', 'age');
}
}校验失败抛出 Viswoole\Core\Exception\ValidateException,错误消息携带参数名上下文(见下文错误消息)。debug 模式下错误消息还会附带参数位置与方法签名信息。
类型校验
参数类型声明本身就是第一道校验(Validate::check),支持:
| 类型类别 | 示例 | 说明 |
|---|---|---|
| 内置类型 | intstringboolfloatarrayobjectnulltruefalse | 基础标量与复合类型,类型别名(booleanintegerdouble)同样支持 |
| 扩展内置类型 | iterablemixedcallableClosure | 完整 PHP 内置类型 |
| 联合类型 | int|string | 依次尝试每个类型 |
| 交集类型 | Countable&Stringable | 值必须 instanceof 所有类型 |
| 枚举 | Status::class | 支持枚举实例、名称字符串(大小写不敏感)和数字索引三种输入 |
| 类/接口 | RequestInterface | 已是实例直接通过;否则尝试通过容器创建实例 |
校验往往伴随类型转换:如 #[Min(1)] int $page 收到字符串 "5" 时会转为 int 后返回。
内置规则
内置规则位于 Viswoole\Core\Validate\Rules 命名空间,均为可标注在方法参数上的 Attribute(同时声明了属性目标,可供自定义校验场景使用)。所有规则的构造参数末位均为 string $message = '',用于自定义错误消息。
数值规则
| 规则 | 构造参数 | 校验行为 |
|---|---|---|
Min | min: int|float | 值必须 ≥ min;非数值报错;返回转换后的 int/float |
Max | max: int|float | 值必须 ≤ max;非数值报错;返回转换后的 int/float |
Between | start: int|float, end: int|float | 值必须介于 start - end(含边界) |
NotBetween | start: int|float, end: int|float | 值必须不在 start - end 之间 |
字符串与集合规则
| 规则 | 构造参数 | 校验行为 |
|---|---|---|
Length | min: int, max: ?int = null | 字符串先 trim 再按 mb_strlen 计长度;数组按元素数计;max 为 null 时要求长度恰为 min |
InArray | haystack: array, strict: bool = true | 值必须在数组中(默认严格比较) |
NotInArray | haystack: array, strict: bool | 值必须不在数组中(strict 无默认值,必须显式传入) |
Regex | pattern: string | 值必须为匹配 $pattern 正则的字符串 |
ArrayItem | types: array|Type | 数组元素逐项按类型校验(复用 Validate::check),返回校验后的数组 |
预置格式规则
以下规则均继承 Regex,本质是预置了正则的模式校验,构造参数只有可选的 message:
| 规则 | 默认消息 | 校验内容 |
|---|---|---|
Mobile | {:name} 必须是有效的手机号 | 大陆手机号 /^1[3-9]\d{9}$/ |
IdCard | {:name} 必须是有效的身份证号码 | 15/18 位身份证;18 位额外验证校验码 |
Chinese | {:name} 必须由汉字组成 | 全部为汉字 |
Alpha | {:name} 必须由字母组成 | 全部为英文字母 |
AlphaNumber | {:name} 只能由字母或数字组成 | 字母与数字组合 |
日期规则
| 规则 | 构造参数 | 校验行为 |
|---|---|---|
DateFormat | format: string | 值必须是符合 format(如 Y-m-d)的日期字符串,严格验证(格式化回读必须一致) |
DateAfter | datetime: string|int | 值必须晚于基准时间 |
DateBefore | datetime: string|int | 值必须早于基准时间 |
DateAfter/DateBefore 的 datetime 基准支持三种形式:Y-m-d H:i:s 格式字符串、Unix 时间戳,以及 +N / -N 相对当前时间的秒数偏移:
use Viswoole\Core\Validate\Rules\{DateFormat, DateAfter, DateBefore};
public function search(
#[DateFormat('Y-m-d'), DateAfter('2024-01-01 00:00:00')] string $start,
#[DateBefore('+3600')] string $deadline = '', // 基准:当前时间 + 1 小时
): array { /* ... */ }相对时间基准在规则实例化时解析
+N/-N 偏移在规则 Attribute 实例化时(构造函数内)转换为具体时间点。由于规则实例由容器预建缓存、跨请求复用(见下文「规则实例无状态约束」),偏移基准会固定在首次实例化的时刻。常驻内存服务中请改用固定时间字符串或时间戳基准,避免基准时间过期。
过滤规则
| 规则 | 构造参数 | 校验行为 |
|---|---|---|
Filter | filter: int, options: array|int = 0 | 基于 filter_var() 校验;支持 FILTER_VALIDATE_EMAIL(邮箱)、FILTER_VALIDATE_URL(URL)、FILTER_VALIDATE_IP(IP)、FILTER_VALIDATE_INT、FILTER_VALIDATE_BOOL、FILTER_VALIDATE_FLOAT、FILTER_VALIDATE_REGEXP、FILTER_VALIDATE_DOMAIN、FILTER_VALIDATE_MAC |
#[Filter(FILTER_VALIDATE_EMAIL)] string $email,错误消息与 {:name} 占位符
内置规则的默认文案内嵌 {:name} 占位符。校验失败时,框架(Validate::withContext())将 {:name} 替换为 $参数名:
public function create(
#[Length(2, 20)] string $name,
) {}
// 传入 '张' 时抛出:
// ValidateException: $name 长度必须在 2 到 20 之间自定义消息同样支持占位符:
public function create(
// 失败时输出:$name 长度必须在 2 到 20 之间
#[Length(2, 20, message: '{:name} 长度必须在 2 到 20 之间')] string $name,
// 未使用占位符时消息原样输出,框架不会自动附加参数名
#[Mobile(message: '请输入正确的手机号')] string $phone,
) {}占位符可出现多次;类型错误(如 int 参数收到 null)由框架统一附加 $参数名 前缀。
自定义规则
继承 Viswoole\Core\Validate\BaseValidateRule 并实现 validate() 方法即可创建自定义规则。基类结构:
| 成员 | 签名 | 说明 |
|---|---|---|
| 构造函数 | __construct(string $message = '') | $message 为自定义错误消息,支持 {:name} 占位符 |
validate | abstract validate(mixed $value): mixed | 校验逻辑;可返回转换后的值 |
error | protected error(?string $message = null): void | 抛出 ValidateException;优先使用构造时传入的 $message,否则使用 $message 参数,都未提供时为 {:name} 验证失败 |
use Attribute;
use Viswoole\Core\Validate\BaseValidateRule;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Phone extends BaseValidateRule
{
/**
* 校验手机号格式
*
* @param mixed $value 待校验的值
* @return mixed 校验通过的值(可返回转换后的值)
*/
public function validate(mixed $value): mixed
{
if (!is_string($value) || preg_match('/^1[3-9]\d{9}$/', $value) !== 1) {
// {:name} 会被替换为 $参数名
$this->error('{:name} 手机号格式不正确');
}
return $value;
}
}
// 使用
public function login(
#[Phone] string $mobile,
) {}
// 校验失败抛出:$mobile 手机号格式不正确自定义规则需要参数名时,为 validate() 声明第二个参数即可,框架会自动传入:
public function validate(mixed $value, string $name = ''): mixed
{
// $name 为被校验参数的名称
}带参数的自定义规则通过构造函数声明:
#[Attribute(Attribute::TARGET_PARAMETER)]
class CustomEnum extends BaseValidateRule
{
public function __construct(
private readonly array $allowed,
string $message = '',
) {
parent::__construct($message);
}
public function validate(mixed $value): mixed
{
if (!in_array($value, $this->allowed, true)) {
$values = implode('、', $this->allowed);
$this->error("{:name} 必须是 {$values} 之一");
}
return $value;
}
}
// 使用:#[CustomEnum(['digital', 'physical'])] string $type规则实例无状态约束
规则实例由容器预解析并跨请求缓存复用(Container 的参数元数据缓存,注释明确「注入与规则实例为无状态对象,构造参数即全部状态,跨请求复用安全」)。因此:
- 构造参数即规则的全部状态,校验所需上下文一律通过
validate()参数传入; - 禁止在规则实例属性上保存请求级数据(如「本次校验的原始值」),否则会造成协程间状态泄漏。
HttpServer 扩展
HTTP 模块在核心验证器之上提供了两个补充。
ValidateNull:注入空值校验
Viswoole\HttpServer\AutoInject\ValidateNull 是一个 Trait,为 #[InjectGet]、#[InjectPost]、#[InjectHeader]、#[InjectFile] 注解提供统一的空值校验:注入值不允许为空且实际为 null 时,抛出 ValidateException。它工作在规则校验之前——参数缺失的报错来自这一层,参数值格式错误才轮到验证规则。
FileRule:上传文件校验
Viswoole\HttpServer\Validate\FileRule 校验上传文件的类型、大小和数量,与 #[InjectFile] 配合使用:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fileMime | string | * | 允许的 MIME 类型,多个用 | 分隔,* 不限制;以服务端内容检测(finfo)为准,不信任客户端声明的 Content-Type |
maxSize | int | 0 | 文件最大字节数,≤ 0 不限制 |
count | int | 0 | 要求的文件数量,≤ 0 不限制 |
message | string | '' | 自定义错误消息 |
use Viswoole\HttpServer\AutoInject\InjectFile;
use Viswoole\HttpServer\Validate\FileRule;
public function upload(
#[InjectFile, FileRule(fileMime: 'image/jpeg|image/png', maxSize: 2097152)] array $images,
) {}