参数校验
概念说明
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);
}
}