创建控制器
本教程带你从零编写一个 Viswoole 控制器:放到约定目录、用注解注册路由、声明方法参数接收请求数据,并理解框架按什么顺序解析这些参数。
控制器放在哪里
框架启动时自动扫描 app/Controller 目录,解析类上的路由注解并注册路由(源码 Viswoole\Router\RouteLoader)。控制器按惯例放在 App\Controller 命名空间下:
app/
└── Controller/
├── UserController.php
└── Order/
└── PayController.php控制器类必须标注路由注解(#[Controller] 或 #[AutoController]),否则不会被扫描注册。新增或修改控制器后需重启服务才能生效。
编写第一个控制器
#[AutoController] 会把类的全部 public 方法自动注册为路由:
<?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:
{ "id": 1, "name": "Viswoole" }未显式指定 prefix 时,类级前缀默认取控制器类名、方法路径默认取方法名;路径匹配是否区分大小写由 router.case_sensitive 配置控制。
两种注册模式
| 模式 | 注解 | 注册规则 | 适用场景 |
|---|---|---|---|
| 自动控制器 | #[AutoController] | 所有 public 方法自动注册 | 快速搭建 API、CRUD 接口 |
| 手动控制器 | #[Controller] | 仅注册带 #[RouteMapping] 的方法 | 精确控制对外暴露的接口 |
#[Controller] 配合 #[RouteMapping] 的示例:
<?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] 共享以下构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| prefix | string|null | 类名/方法名 | 路径前缀,支持动态变量 |
| id | string|null | 自动生成 | 路由 ID,编程式分组引用时需手动指定 |
| parentId | string|null | null | 父级分组 ID,必须是分组路由 ID |
| method | string|string[]|null | 继承全局配置 | HTTP 请求方法,如 'GET'、['GET', 'POST'] |
| middlewares | array|null | null | 路由级中间件(注解参数不支持闭包) |
| patterns | array|null | null | 动态变量正则约束,键为变量名 |
| meta | array|null | null | 路由元数据 |
| suffix | string|string[]|null | null | 伪静态后缀 |
| domain | string|string[]|null | null | 域名校验 |
| hidden | bool | false | 是否在 API 文档中隐藏 |
| title | string|null | PHPDoc 首行 | 路由标题 |
| description | string|null | PHPDoc 正文 | 路由描述 |
| sort | int | 0 | 排序权重,数值越大越靠前 |
#[RouteMapping] 额外支持 author、createdAt、updatedAt、tags、status 等 API 文档字段。完整参数说明见注解路由。
控制器的实例化时机
控制器不是服务启动时创建的,而是请求命中路由后由容器(Container)反射调用(源码 Container::invokeMethod() → invokeClass()):
- 每个请求都会创建全新实例:实例单例缓存写入当前请求根协程的上下文,请求结束自动销毁;
- 构造函数支持依赖注入:直接声明服务类型的参数即可;
- 若希望绕过单例缓存、每次调用都新建,可在类中定义
ALLOW_NEW_INSTANCE = true常量。
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):
① 取值(路由变量/默认值) → ② 前置注入注解 → ③ 类型校验与类型注入 → ④ 验证规则注解① 取值:动态路由变量与默认值
框架先按「参数名或参数位置」查找显式传入的参数。HTTP 场景下,路由匹配到的动态路由变量会以 变量名 => 值 的形式并入这些参数(源码 Router::dispatch() 将命名捕获组与路由声明的变量求交集后合并)。未命中时取方法默认值,都没有则为 null。
// 路由: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)),因此实际值的优先级为:
注解数据源(GET/POST/请求头/文件) > 动态路由变量 > 方法默认值③ 类型校验与类型注入
- 内置类型(
int、string、bool、float、array等):自动转换并校验;非可空参数取到null时直接抛出ValidateException; - 类/接口类型:当前值不是对应实例时,由容器
make()解析实例(源码Validate::class())——RequestInterface、ResponseInterface以及各业务服务都由此注入。
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
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 响应对象。
下一步
- 自动注入注解:
#[InjectGet]等注解的完整行为与错误消息 - Request 请求对象:需要手动读取请求数据时使用
- 文件上传:处理
multipart/form-data文件上传
