创建控制器

本教程带你从零编写一个 Viswoole 控制器:放到约定目录、用注解注册路由、声明方法参数接收请求数据,并理解框架按什么顺序解析这些参数。

控制器放在哪里

框架启动时自动扫描 app/Controller 目录,解析类上的路由注解并注册路由(源码 Viswoole\Router\RouteLoader)。控制器按惯例放在 App\Controller 命名空间下:

text
app/
└── Controller/
    ├── UserController.php
    └── Order/
        └── PayController.php

控制器类必须标注路由注解(#[Controller]#[AutoController]),否则不会被扫描注册。新增或修改控制器后需重启服务才能生效。

编写第一个控制器

#[AutoController] 会把类的全部 public 方法自动注册为路由:

php
<?php
declare(strict_types=1);

namespace App\Controller;

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

#[AutoController(prefix: 'user')]
class UserController
{
    /**
     * 用户详情
     *
     * @param int $id 用户ID
     */
    public function show(#[InjectGet] int $id): array
    {
        return ['id' => $id, 'name' => 'Viswoole'];
    }
}

访问 GET /user/show?id=1,返回 JSON:

json
{ "id": 1, "name": "Viswoole" }

未显式指定 prefix 时,类级前缀默认取控制器类名、方法路径默认取方法名;路径匹配是否区分大小写由 router.case_sensitive 配置控制。

两种注册模式

模式注解注册规则适用场景
自动控制器#[AutoController]所有 public 方法自动注册快速搭建 API、CRUD 接口
手动控制器#[Controller]仅注册带 #[RouteMapping] 的方法精确控制对外暴露的接口

#[Controller] 配合 #[RouteMapping] 的示例:

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\Router\Annotation\Controller;
use Viswoole\Router\Annotation\RouteMapping;
use App\Middleware\AuthMiddleware; // 应用自定义中间件

#[Controller(prefix: 'admin', middlewares: [AuthMiddleware::class])]
class AdminController
{
    /** 登录页 */
    #[RouteMapping('login', method: ['GET', 'POST'])]
    public function login(): string
    {
        return '<h1>Login</h1>';
    }

    /**
     * 用户仪表盘,动态变量 {id} 按参数名注入 $id
     */
    #[RouteMapping('dashboard/{id}', method: 'GET', patterns: ['id' => '\d+'])]
    public function dashboard(int $id): array
    {
        return ['id' => $id];
    }

    // 内部方法,无 #[RouteMapping] 注解,不会暴露为路由
    private function helper(): void {}
}

注解公共参数

#[Controller]#[RouteMapping] 共享以下构造参数:

参数类型默认值说明
prefixstring|null类名/方法名路径前缀,支持动态变量
idstring|null自动生成路由 ID,编程式分组引用时需手动指定
parentIdstring|nullnull父级分组 ID,必须是分组路由 ID
methodstring|string[]|null继承全局配置HTTP 请求方法,如 'GET'['GET', 'POST']
middlewaresarray|nullnull路由级中间件(注解参数不支持闭包)
patternsarray|nullnull动态变量正则约束,键为变量名
metaarray|nullnull路由元数据
suffixstring|string[]|nullnull伪静态后缀
domainstring|string[]|nullnull域名校验
hiddenboolfalse是否在 API 文档中隐藏
titlestring|nullPHPDoc 首行路由标题
descriptionstring|nullPHPDoc 正文路由描述
sortint0排序权重,数值越大越靠前

#[RouteMapping] 额外支持 authorcreatedAtupdatedAttagsstatus 等 API 文档字段。完整参数说明见注解路由

控制器的实例化时机

控制器不是服务启动时创建的,而是请求命中路由后由容器(Container)反射调用(源码 Container::invokeMethod()invokeClass()):

  • 每个请求都会创建全新实例:实例单例缓存写入当前请求根协程的上下文,请求结束自动销毁;
  • 构造函数支持依赖注入:直接声明服务类型的参数即可;
  • 若希望绕过单例缓存、每次调用都新建,可在类中定义 ALLOW_NEW_INSTANCE = true 常量。
php
use App\Service\OrderService;
use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\Router\Annotation\AutoController;

#[AutoController(prefix: 'order')]
class OrderController
{
    public function __construct(private readonly OrderService $service)
    {
    }

    public function detail(#[InjectGet] int $id): array
    {
        return $this->service->find($id);
    }
}

不要用静态属性保存请求级状态

控制器实例虽按请求销毁,但 Worker 进程常驻内存,任何以 static 属性保存的状态都会跨请求共享,请务必避免。

方法参数解析顺序

控制器方法参数由容器的 injectParams() 按固定流水线解析(源码 Viswoole\Core\Container):

text
① 取值(路由变量/默认值) → ② 前置注入注解 → ③ 类型校验与类型注入 → ④ 验证规则注解

① 取值:动态路由变量与默认值

框架先按「参数名或参数位置」查找显式传入的参数。HTTP 场景下,路由匹配到的动态路由变量会以 变量名 => 值 的形式并入这些参数(源码 Router::dispatch() 将命名捕获组与路由声明的变量求交集后合并)。未命中时取方法默认值,都没有则为 null

php
// 路由:GET /order/show/1024
#[RouteMapping('show/{id}', method: 'GET', patterns: ['id' => '\d+'])]
public function show(int $id): array   // {id} 按参数名注入 $id,无需任何注解
{
    return ['id' => $id];
}

② 前置注入注解

参数上标注了 #[InjectGet]#[InjectPost] 等实现了 PreInjectInterface 的注解时,框架依次调用其 inject($name, $value, $allowNull) 方法。注解内部以第 ① 步的值作为兜底,从指定数据源重新取值(如 Request::get($name, $value)),因此实际值的优先级为:

text
注解数据源(GET/POST/请求头/文件) > 动态路由变量 > 方法默认值

③ 类型校验与类型注入

  • 内置类型intstringboolfloatarray 等):自动转换并校验;非可空参数取到 null 时直接抛出 ValidateException
  • 类/接口类型:当前值不是对应实例时,由容器 make() 解析实例(源码 Validate::class())——RequestInterfaceResponseInterface 以及各业务服务都由此注入。
php
use Viswoole\HttpServer\Contract\RequestInterface;

public function info(RequestInterface $request, OrderService $service): array
{
    return $service->describe($request->getMethod());
}

④ 验证规则注解

参数上标注了 BaseValidateRule 子类(如 #[Min]#[Length]#[FileRule])时依次校验,规则还可以对值做转换。验证失败抛出 ValidateException,由异常处理器渲染为 400 的 JSON 响应。

综合示例

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\Core\Validate\Rules\Min;
use Viswoole\HttpServer\AutoInject\{InjectFile, InjectGet, InjectPost};
use Viswoole\HttpServer\Contract\RequestInterface;
use Viswoole\HttpServer\Message\UploadedFile;
use Viswoole\Router\Annotation\AutoController;
use Viswoole\Router\Annotation\RouteMapping;

#[AutoController(prefix: 'order')]
class OrderController
{
    #[RouteMapping(method: 'POST')]
    public function create(
        #[InjectGet] ?string $couponCode,      // GET 参数,缺失时为 null
        #[InjectPost, Min(1)] int $productId,  // POST 参数 + 验证规则
        #[InjectFile] UploadedFile $receipt,   // 上传文件
        RequestInterface $request,             // 容器类型注入
    ): array {
        return [
            'coupon' => $couponCode,
            'product' => $productId,
            'file' => $receipt->getClientFilename(),
            'ip' => $request->ip(),
        ];
    }
}

返回值处理

控制器方法的返回值由框架按类型自动处理(源码 HttpEventHandle::handleResponse()):

返回类型处理方式
ResponseInterface直接调用 send() 发送
数组或对象自动 JSON 编码后发送
其他标量转为字符串作为响应体

完整规则与响应控制方法见 Response 响应对象

下一步