自动参数注入

Viswoole 框架提供了强大的自动参数注入功能,通过注解声明式地从 HTTP 请求的不同数据源获取参数,并自动进行类型转换和验证。这大大简化了控制器代码,让开发者专注于业务逻辑。

注入注解概览

注解数据源接口典型用途
#[InjectGet]URL 查询参数(?key=valueQueryParamInterface获取 GET 请求参数
#[InjectPost]POST 请求体(JSON/表单)BodyParamInterface获取 POST 请求数据
#[InjectHeader]HTTP 请求头HeaderParamInterface获取自定义请求头
#[InjectFile]上传文件FileParamInterface获取上传的文件对象

基础用法

InjectGet - 查询参数注入

从 URL 查询字符串中获取参数:

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\Router\Annotation\AutoController;

#[AutoController]
class SearchController
{
    /**
     * 搜索接口
     *
     * GET /search/query?keyword=php&page=1&size=10
     *
     * @param string $keyword 搜索关键词
     * @param int $page 页码
     * @param int $size 每页数量
     * @return array 搜索结果
     */
    public static function query(
        #[InjectGet] string $keyword,
        #[InjectGet] int $page = 1,        // 支持默认值
        #[InjectGet] int $size = 10         // 支持默认值
    ): array {
        return [
            'keyword' => $keyword,
            'page' => $page,
            'size' => $size,
            'results' => []
        ];
    }
}

请求示例:

text
GET /search/query?keyword=viswoole&page=2&size=20

InjectPost - 请求体注入

从 POST 请求体中获取参数(支持 JSON 和表单):

php
<?php
declare(strict_types=1);

namespace App\Controller;

use App\Interface\UserInfo;
use Viswoole\HttpServer\AutoInject\InjectPost;
use Viswoole\Router\Annotation\AutoController;

#[AutoController]
class UserController
{
    /**
     * 创建用户
     *
     * POST /user/create
     * Content-Type: application/json
     * Body: { "name": "张三", "email": "test@example.com" }
     *
     * @param UserInfo $userInfo 用户信息 DTO
     * @return array 创建结果
     */
    #[RouteMapping(method: 'POST')]
    public static function create(#[InjectPost] UserInfo $userInfo): array
    {
        // userInfo 已经过验证和类型转换
        return [
            'name' => $userInfo->name,
            'email' => $userInfo->email ?? '',
            'message' => '创建成功'
        ];
    }

    /**
     * 简单参数注入
     *
     * POST /user/update
     * Body: { "id": 100, "name": "新名称" }
     */
    #[RouteMapping(method: 'POST')]
    public static function update(
        #[InjectPost] int $id,
        #[InjectPost] string $name
    ): array {
        return ['id' => $id, 'name' => $name];
    }
}

InjectHeader - 请求头注入

从 HTTP 请求头中获取参数:

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\HttpServer\AutoInject\InjectHeader;
use Viswoole\Router\Annotation\AutoController;

#[AutoController]
class ApiController
{
    /**
     * API 接口(需要认证)
     *
     * Headers:
     *   Authorization: Bearer xxxxx
     *   X-Request-Id: uuid-string
     *   Accept-Language: zh-CN
     */
    public static function data(
        #[InjectHeader] string $authorization,
        #[InjectHeader] string $xRequestId,
        #[InjectHeader] ?string $acceptLanguage = 'zh-CN'
    ): array {
        // 解析 Bearer Token
        $token = str_replace('Bearer ', '', $authorization);

        return [
            'token' => substr($token, 0, 10) . '...',
            'request_id' => $xRequestId,
            'language' => $acceptLanguage
        ];
    }
}

注意:InjectHeader 无构造函数参数,参数名必须与 header 名一致(不区分大小写)。例如参数名 $xRequestId 会匹配请求头 X-Request-Id

支持的数据类型

基本类型

框架自动将字符串参数转换为声明的类型:

php
public static function test(
    #[InjectGet] string $name,      // 字符串
    #[InjectGet] int $age,          // 整数
    #[InjectGet] float $price,      // 浮点数
    #[InjectGet] bool $isActive,    // 布尔值("1"/"true" → true)
    #[InjectGet] ?int $optionalId   // 可空类型
): array { ... }

类型转换规则:

  • int: 字符串数字 → 整数(如 "123"123
  • float: 字符串数字 → 浮点数(如 "19.99"19.99
  • bool: "1", "true", "on"true; 其他 → false
  • string: 保持原值(经过 XSS 过滤)

数组类型

对于数组参数,使用数组语法传递:

php
/**
 * 批量操作
 *
 * POST /batch/delete
 * Body: { "ids": [1, 2, 3, 4, 5] }
 */
#[RouteMapping(method: 'POST')]
public static function batchDelete(#[InjectPost] array $ids): array
{
    return ['deleted' => count($ids), 'ids' => $ids];
}

对象类型(DTO)

将请求数据自动映射到 DTO 对象,这是最强大的特性:

定义 DTO 类

php
<?php
declare(strict_types=1);

namespace App\Interface;

use Viswoole\Core\Validate\Rules\Chinese;

/**
 * 创建用户 DTO
 *
 * 属性会根据验证注解自动校验
 */
class CreateUserDto
{
    /**
     * @param Chinese $name 姓名(必须是中文)
     * @param string $email 邮箱地址(必须符合邮箱格式)
     * @param string $gender 性别
     * @param int|null $age 年龄(可选)
     */
    public function __construct(
        #[Chinese(message: '姓名必须是中文')] public readonly string $name,
        #[Regex(pattern: "^\w+([-+.]\w+)*@\w+[.]{1,3}\w+$", message: "邮箱格式不正确")] public readonly string $email,
        #[Alpha] public readonly string $gender,
        public readonly ?int $age = null
    ) {}
}

在控制器中使用

php
/**
 * 创建用户
 *
 * POST /user/create
 * Content-Type: application/json
 * Body: {
 *   "name": "张三",
 *   "email": "zhangsan@example.com",
 *   "gender": "male",
 *   "age": 25
 * }
 */
#[RouteMapping(method: 'POST')]
public static function create(#[InjectPost] CreateUserDto $dto): array
{
    // 所有属性已经过验证,可直接安全使用
    return [
        'name' => $dto->name,           // "张三"
        'email' => $dto->email,         // "zhangsan@example.com"
        'gender' => $dto->gender, // "male"
        'age' => $dto->age              // 25
    ];
}

验证失败时: 如果请求数据不符合验证规则,框架会抛出 ValidateException,返回 400 错误和详细的错误信息。

空值校验

必填参数 vs 可选参数

当参数不允许为空但值为 null 时,框架会抛出 ValidateException

php
// ✅ 必填:参数缺失或为空时报错
public static function required(#[InjectGet] string $name): string
{
    return $name;
}

// ✅ 可选:使用默认值
public static function optional(#[InjectGet] string $name = 'default'): string
{
    return $name;
}

// ✅ 可空:允许 null 值
public static function nullable(#[InjectGet] ?string $name = null): ?string
{
    return $name;
}

错误响应示例

当必填参数缺失时:

json
{
  "code": 400,
  "message": "(GET)请求参数name不能为空",
  "error": "ValidateException"
}

默认值

所有注入类型都支持默认值:

php
public static function list(
    #[InjectGet] int $page = 1,           // 默认第 1 页
    #[InjectGet] int $size = 10,          // 默认每页 10 条
    #[InjectGet] string $sort = 'created_at', // 默认排序字段
    #[InjectGet] string $order = 'desc',  // 默认降序
    #[InjectGet] ?string $keyword = null   // 可选搜索关键词
): array {
    return compact('page', 'size', 'sort', 'order', 'keyword');
}

完整示例:综合使用所有注入类型

php
<?php
declare(strict_types=1);

namespace App\Controller;

use App\Interface\OrderDto;
use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\HttpServer\AutoInject\InjectPost;
use Viswoole\HttpServer\AutoInject\InjectHeader;
use Viswoole\HttpServer\AutoInject\InjectFile;
use Viswoole\HttpServer\Message\UploadedFile;
use Viswoole\HttpServer\Contract\ResponseInterface;
use Viswoole\Router\Annotation\AutoController;
use Viswoole\Router\Annotation\RouteMapping;
use App\Response;

/**
 * 订单控制器 - 演示所有注入类型
 */
#[AutoController(prefix: 'api/v1')]
class OrderController
{
    /**
     * 创建订单(综合示例)
     *
     * POST /api/v1/order/create
     *
     * Query: ?couponCode=SAVE20
     * Headers:
     *   X-Request-Id: req-123456
     *   Authorization: Bearer token-xxx
     * Body (JSON): {
     *   "productId": 1001,
     *   "quantity": 2,
     *   "address": "北京市朝阳区..."
     * }
     * Files: receipt (发票文件)
     */
    #[RouteMapping(method: 'POST')]
    public static function create(
        // GET 参数:优惠码
        #[InjectGet] ?string $couponCode = null,

        // Header 参数
        #[InjectHeader] string $xRequestId,
        #[InjectHeader] string $authorization,

        // POST 参数(DTO 对象)
        #[InjectPost] OrderDto $order,

        // 文件上传
        #[InjectFile] UploadedFile $receipt,

        // 依赖注入:Response 对象
        Response $response
    ): ResponseInterface {
        // 处理业务逻辑...
        $token = str_replace('Bearer ', '', $authorization);
        $orderId = uniqid('order_');

        // 移动上传文件
        $uploadPath = app()->getRootPath() . "/runtime/uploads/receipts/{$orderId}_receipt.pdf";
        $receipt->moveTo($uploadPath);

        return $response->json([
            'order_id' => $orderId,
            'request_id' => $xRequestId,
            'product' => $order->productId,
            'quantity' => $order->quantity,
            'coupon' => $couponCode ?? '未使用',
            'receipt_file' => $receipt->getClientFilename(),
            'token_prefix' => substr($token, 0, 8) . '...'
        ]);
    }
}

最佳实践

1. 使用 DTO 对象管理复杂请求体

对于包含多个字段的请求,始终定义 DTO 类:

php
// ✅ 推荐:DTO 对象
public static function create(#[InjectPost] CreateUserDto $dto): array { ... }

// ❌ 避免:过多独立参数
public static function create(
    #[InjectPost] string $name,
    #[InjectPost] string $email,
    #[InjectPost] string $phone,
    #[InjectPost] int $age,
    #[InjectPost] string $address,
    #[InjectPost] string $city,
    // ... 更多参数
): array { ... }

2. 合理设置默认值

php
// ✅ 推荐:合理的默认值
public static function list(
    #[InjectGet] int $page = 1,       // 从第一页开始
    #[InjectGet] int $size = 10,      // 合理的分页大小
    #[InjectGet] string $sort = 'id', // 默认按 ID 排序
    #[InjectGet] string $order = 'DESC' // 默认降序
): array { ... }

3. 使用可空类型处理可选字段

php
// ✅ 推荐:可选字段使用 ?type
public static function search(
    #[InjectGet] string $keyword,
    #[InjectGet] ?string $category = null,  // 可选分类
    #[InjectGet] ?int $minPrice = null,     // 可选最低价
    #[InjectGet] ?int $maxPrice = null      // 可选最高价
): array { ... }

4. 验证注解与 DTO 结合使用

在 DTO 构造函数参数上添加验证注解:

php
class ProductDto
{
    public function __construct(
        #[Min(1)] public readonly int $id,
        #[Length(2, 100)] public readonly string $name,
        #[Min(0.01)] public readonly float $price,
        #[InArray(['active', 'inactive'])] public readonly string $status
    ) {}
}

常见问题

Q: 可以同时使用多个相同类型的注入注解吗?

A: 可以,每个参数使用独立的注解:

php
public static function test(
    #[InjectGet] string $param1,
    #[InjectGet] int $param2,
    #[InjectGet] bool $param3
): array { ... }

Q: 如何获取原始未过滤的值?

A: 直接使用 Request 对象的方法,绕过注入系统:

php
use Viswoole\HttpServer\Facade\Request;

public static function raw(): mixed
{
    // 获取未经 htmlspecialchars 过滤的原始值
    return Request::post('content', null, []); // 空数组表示不使用过滤器
}

Q: 注入的参数会被 XSS 过滤吗?

A: 是的,默认会对字符串类型应用 htmlspecialchars 过滤。如果需要原始值(如富文本内容),使用 Request 对象并传入空的过滤器数组。

Q: DTO 对象支持嵌套吗?

A: 支持,只要嵌套对象的构造函数也符合注入规范即可。详见框架的验证规则文档。