验证器

验证器提供声明式的参数校验:以 PHP 注解(Attribute)把校验规则直接标注在方法参数上,容器注入参数时自动执行校验,失败即抛出异常。校验逻辑因此与业务代码分离,规则紧贴参数定义,可读性与可维护性兼得。

本文依据框架源码 src/Core/Validate.phpsrc/Core/Validate/BaseValidateRule.phpsrc/Core/Validate/Rules/*src/Core/Container.php(参数注入链路)、src/HttpServer/AutoInject/ValidateNull.php 编写。

工作原理

容器解析方法参数时(Container::injectParams()),对每个参数依次执行:

text
取值(命名/位置/默认值)→ 前置注入注解 → 类型校验(Validate::check)→ 规则校验(Validate::checkRules)

规则校验在容器注入参数时自动触发,无需手动调用:

php
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 = '',用于自定义错误消息。

数值规则

规则构造参数校验行为
Minmin: int|float值必须 ≥ min;非数值报错;返回转换后的 int/float
Maxmax: int|float值必须 ≤ max;非数值报错;返回转换后的 int/float
Betweenstart: int|float, end: int|float值必须介于 start - end(含边界)
NotBetweenstart: int|float, end: int|float值必须start - end 之间

字符串与集合规则

规则构造参数校验行为
Lengthmin: int, max: ?int = null字符串先 trim 再按 mb_strlen 计长度;数组按元素数计;max 为 null 时要求长度恰为 min
InArrayhaystack: array, strict: bool = true值必须在数组中(默认严格比较)
NotInArrayhaystack: array, strict: bool值必须在数组中(strict 无默认值,必须显式传入)
Regexpattern: string值必须为匹配 $pattern 正则的字符串
ArrayItemtypes: array|Type数组元素逐项按类型校验(复用 Validate::check),返回校验后的数组

预置格式规则

以下规则均继承 Regex,本质是预置了正则的模式校验,构造参数只有可选的 message

规则默认消息校验内容
Mobile{:name} 必须是有效的手机号大陆手机号 /^1[3-9]\d{9}$/
IdCard{:name} 必须是有效的身份证号码15/18 位身份证;18 位额外验证校验码
Chinese{:name} 必须由汉字组成全部为汉字
Alpha{:name} 必须由字母组成全部为英文字母
AlphaNumber{:name} 只能由字母或数字组成字母与数字组合

日期规则

规则构造参数校验行为
DateFormatformat: string值必须是符合 format(如 Y-m-d)的日期字符串,严格验证(格式化回读必须一致)
DateAfterdatetime: string|int值必须晚于基准时间
DateBeforedatetime: string|int值必须早于基准时间

DateAfter/DateBeforedatetime 基准支持三种形式:Y-m-d H:i:s 格式字符串、Unix 时间戳,以及 +N / -N 相对当前时间的秒数偏移:

php
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 实例化时(构造函数内)转换为具体时间点。由于规则实例由容器预建缓存、跨请求复用(见下文「规则实例无状态约束」),偏移基准会固定在首次实例化的时刻。常驻内存服务中请改用固定时间字符串或时间戳基准,避免基准时间过期。

过滤规则

规则构造参数校验行为
Filterfilter: int, options: array|int = 0基于 filter_var() 校验;支持 FILTER_VALIDATE_EMAIL(邮箱)、FILTER_VALIDATE_URL(URL)、FILTER_VALIDATE_IP(IP)、FILTER_VALIDATE_INTFILTER_VALIDATE_BOOLFILTER_VALIDATE_FLOATFILTER_VALIDATE_REGEXPFILTER_VALIDATE_DOMAINFILTER_VALIDATE_MAC
php
#[Filter(FILTER_VALIDATE_EMAIL)] string $email,

错误消息与 {:name} 占位符

内置规则的默认文案内嵌 {:name} 占位符。校验失败时,框架(Validate::withContext())将 {:name} 替换为 $参数名

php
public function create(
    #[Length(2, 20)] string $name,
) {}

// 传入 '张' 时抛出:
// ValidateException: $name 长度必须在 2 到 20 之间

自定义消息同样支持占位符:

php
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} 占位符
validateabstract validate(mixed $value): mixed校验逻辑;可返回转换后的值
errorprotected error(?string $message = null): void抛出 ValidateException;优先使用构造时传入的 $message,否则使用 $message 参数,都未提供时为 {:name} 验证失败
php
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() 声明第二个参数即可,框架会自动传入:

php
public function validate(mixed $value, string $name = ''): mixed
{
    // $name 为被校验参数的名称
}

带参数的自定义规则通过构造函数声明:

php
#[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] 配合使用:

参数类型默认值说明
fileMimestring*允许的 MIME 类型,多个用 | 分隔,* 不限制;以服务端内容检测(finfo)为准,不信任客户端声明的 Content-Type
maxSizeint0文件最大字节数,≤ 0 不限制
countint0要求的文件数量,≤ 0 不限制
messagestring''自定义错误消息
php
use Viswoole\HttpServer\AutoInject\InjectFile;
use Viswoole\HttpServer\Validate\FileRule;

public function upload(
    #[InjectFile, FileRule(fileMime: 'image/jpeg|image/png', maxSize: 2097152)] array $images,
) {}

下一步