参数校验

概念说明

Viswoole 的校验系统提供 声明式参数校验 能力,通过 PHP 8 的 Attribute(注解)在函数参数上声明校验规则,由容器在调用方法时自动执行校验。这种方式将校验逻辑从业务代码中分离出来,使代码更加简洁和可维护。

校验系统的核心组件:

组件说明
Validate校验引擎,提供 check() 方法执行类型与规则校验
BaseValidateRule自定义规则的基类,所有校验规则均继承此类
内置规则框架预置的常用校验规则集合

工作原理

校验流程

text
方法调用 invoke([obj, 'method'], $params)


 反射获取方法参数列表


 遍历每个参数的 Attributes


 执行类型检查 + 规则校验

       ├── 类型匹配? ──否──▶ 抛出 ValidateException

       └── 规则通过? ──否──▶ 抛出 ValidateException(含错误信息)


 校验通过,执行方法体

支持的类型校验

Validate::check() 支持丰富的类型声明:

类型类别示例说明
内置类型int, string, bool, float, array基础标量和复合类型
联合类型int|string参数可为多种类型之一
交集类型A&B参数必须同时满足多个类型约束
枚举StatusEnum值必须是枚举的成员之一
类/接口实例RequestInterface值必须是指定类或接口的实例

内置校验规则

规则类说明示例
Alpha纯字母用户名、姓名
AlphaNumber字母+数字编码、账号
ArrayItem数组元素校验列表项类型约束
Between数值范围年龄、金额区间
Chinese中文字符姓名、地址
DateAfter日期晚于指定日期开始时间
DateBefore日期早于指定日期截止时间
DateFormat日期格式出生日期
Filter基于 PHP filter_var 的过滤器校验email、url 等
IdCard身份证号码实名认证
InArray枚举值范围状态、类型
Length字符串长度密码、标题
Max最大值上限约束
Min最小值下限约束
Mobile手机号联系方式
NotBetween排除范围排除特殊区间
NotInArray排除枚举值黑名单
Regex正则表达式自定义格式

代码示例

基本类型校验

php
use Viswoole\Core\Validate\Rules\{Min, Max, Length, Between};

class UserController
{
    /**
     * 创建用户
     *
     * @param string $name - 用户名,2-20 个字符
     * @param int $age - 年龄,18-120 之间
     * @param float $score - 分数,0-100 之间
     */
    public function create(
        #[Length(2, 20)] string $name,
        #[Between(18, 120)] int $age,
        #[Min(0), Max(100)] float $score,
    ): array {
        return compact('name', 'age', 'score');
    }
}

// 容器调用时自动校验
app()->invoke([new UserController(), 'create'], [
    'name' => '张三',
    'age' => 25,
    'score' => 95.5,
]);

// 校验失败会抛出 ValidateException
app()->invoke([new UserController(), 'create'], [
    'name' => '张',          // ❌ 长度不足 2
    'age' => 15,             // ❌ 不在 18-120 范围
    'score' => 150,          // ❌ 超过最大值 100
]);

联合类型与枚举校验

php
enum Status: string
{
    case ACTIVE = 'active';
    case INACTIVE = 'inactive';
    case PENDING = 'pending';
}

class OrderController
{
    /**
     * 查询订单
     *
     * @param int|string $id - 订单ID(数字ID或字符串编号)
     * @param Status $status - 订单状态枚举
     */
    public function list(
        int|string $id,
        Status $status,
    ): array { /* ... */ }
}

// 正确调用
app()->invoke([$controller, 'list'], [
    'id' => 'ORD20240101',  // string 匹配联合类型
    'status' => 'active',    // 自动转为 Status::ACTIVE
]);

复杂规则组合

php
use Viswoole\Core\Validate\Rules\{
    AlphaNumber, InArray, Mobile, Chinese, DateFormat, DateAfter
};

class ProfileController
{
    /**
     * 更新个人资料
     *
     * @param string $nickname - 字母数字组合
     * @param string $realname - 中文真实姓名
     * @param string $phone - 手机号
     * @param string $gender - 性别枚举
     * @param string $birthday - 生日,YYYY-mm-dd 格式且早于今天
     */
    public function update(
        #[AlphaNumber] string $nickname,
        #[Chinese] string $realname,
        #[Mobile] string $phone,
        #[InArray(['male', 'female', 'other'], true)] string $gender,
        #[DateFormat('Y-m-d'), DateAfter('-120 years')] string $birthday,
    ): array { /* ... */ }
}

自定义校验规则

通过继承 BaseValidateRule 创建自定义规则:

php
use Viswoole\Core\Validate\Rules\BaseValidateRule;
use Attribute;

#[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) {
            $this->error('手机号格式不正确');
        }
        return $value;
    }
}

// 使用自定义规则
class AuthController
{
    public function login(
        #[Phone] string $mobile,
        #[Length(4, 6)] string $code,
    ): array { /* ... */ }
}

带参数的自定义规则

php
#[Attribute(Attribute::TARGET_PARAMETER)]
class CustomEnum extends BaseValidateRule
{
    /**
     * @param array<string> $allowed 允许的值列表
     */
    public function __construct(
        private readonly array $allowed,
    ) {}

    public function validate(mixed $value): mixed
    {
        if (!in_array($value, $this->allowed, true)) {
            $values = implode(', ', $this->allowed);
            $this->error("值必须在以下范围内: {$values}");
        }
        return $value;
    }
}

// 使用带参数的规则
class ProductController
{
    public function create(
        #[CustomEnum(['digital', 'physical', 'virtual'])] string $type,
    ): array { /* ... */ }
}

最佳实践

1. 校验规则靠近参数定义

将校验注解直接写在参数上,使校验规则与参数语义紧密关联:

php
// ✅ 推荐:注解紧跟参数,一目了然
public function register(
    #[Length(2, 20)] string $username,
    #[Filter(FILTER_VALIDATE_EMAIL)] string $email,
    #[Min(8), Max(32)] string $password,
) { }

// ❌ 不推荐:将校验逻辑分散在方法体内
public function register(string $username, string $email, string $password)
{
    if (strlen($username) < 2 || strlen($username) > 20) { throw new ... }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new ... }
    if (strlen($password) < 8) { throw new ... }
}

2. 自定义规则提供清晰的错误信息

错误信息应明确指出问题和期望格式:

php
// ✅ 推荐:错误信息具体有用
$this->error('手机号应为11位数字,以1开头');

// ❌ 不推荐:模糊的错误信息
$this->error('参数错误'); // 开发者和用户都不知道哪里错了

3. 组合规则保持合理数量

单个参数上堆砌过多规则会影响可读性:

php
// ✅ 推荐:规则精炼,覆盖关键约束
#[Mobile] string $phone,

// ❌ 不推荐:规则冗余重叠
#[Length(11, 11), Regex('/^1\d{10}$/'), AlphaNumber, Min(10000000000), Max(99999999999)] string $phone,

4. DTO 与校验结合

对于复杂的输入结构,使用 DTO 类集中管理校验:

php
class CreateOrderInput
{
    public function __construct(
        #[InArray(['alipay', 'wechat', 'card'], true)] public readonly string $payMethod,
        #[Min(0.01)] public readonly float $amount,
        #[Length(0, 255)] public readonly string $remark,
        #[DateFormat('Y-m-d')] public readonly ?string $expectedDate = null,
    ) {}
}

class OrderController
{
    public function create(CreateOrderInput $input): JsonResponse
    {
        // $input 的所有属性已在构造时完成校验
        $order = $this->orderService->create($input);
        return json($order);
    }
}