创建控制器

控制器是处理 HTTP 请求的入口点,Viswoole 框架通过 PHP 8 注解(Attribute)机制实现控制器的声明式注册。本节将详细介绍如何创建和配置控制器。

基本结构

控制器文件位置

所有控制器必须位于 app/Controller 目录下,命名空间为 App\Controller

text
app/
└── Controller/
    ├── UserController.php
    ├── OrderController.php
    └── Example.php

最小化控制器示例

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\Router\Annotation\AutoController;

#[AutoController]
class DemoController
{
    /**
     * 首页
     */
    public static function index(): string
    {
        return 'Hello Viswoole!';
    }
}

访问路径:GET /democontroller/index

说明#[AutoController] 未设置 prefix 时,路径使用完整类名小写化(如 DemoControllerdemocontroller),不去除 Controller 后缀。

AutoController 注解

#[AutoController] 是最常用的控制器注解,它会自动将类中所有 public 方法注册为路由。

基础用法

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\Router\Annotation\AutoController;

#[AutoController]
class ProductController
{
    /**
     * 产品列表
     */
    public static function list(): array
    {
        return ['products' => []];
    }

    /**
     * 产品详情
     *
     * @param int $id 产品ID
     */
    public static function detail(#[InjectGet] int $id): array
    {
        return ['id' => $id, 'name' => 'Product'];
    }
}

生成的路由:

  • GET /productcontroller/listProductController::list()
  • GET /productcontroller/detail?id=1ProductController::detail()

配置选项

php
#[AutoController(
    prefix: 'api/v1',      // 路由前缀:/api/v1/product/*
    sort: 100,             // 排序权重(数值越大越靠前)
    middlewares: [          // 中间件列表
        AuthMiddleware::class,
        RateLimitMiddleware::class
    ]
)]
class ApiController
{
    // ...
}

参数说明

参数类型默认值说明
prefixstring''路由前缀,会添加到所有方法路由前
sortint0路由排序权重,数值越大越靠前
middlewaresarray[]应用于该控制器所有方法的中间件

Controller 注解(手动模式)

当需要精确控制哪些方法暴露为路由时,使用 #[Controller] 注解配合 #[RouteMapping]

基础用法

php
<?php
declare(strict_types=1);

namespace App\Controller;

use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\Router\Annotation\Controller;
use Viswoole\Router\Annotation\RouteMapping;

#[Controller(prefix: 'admin')]
class AdminController
{
    /**
     * 登录页面 - 公开访问
     */
    #[RouteMapping('login', method: ['GET', 'POST'])]
    public static function login(): string
    {
        return '<h1>Login</h1>';
    }

    /**
     * 仪表盘 - 需要认证
     */
    #[RouteMapping('dashboard', method: 'GET')]
    #[Auth] // 自定义认证中间件
    public static function dashboard(#[InjectGet] string $tab = 'overview'): array
    {
        return ['tab' => $tab, 'data' => []];
    }

    /**
     * 内部方法 - 不对外暴露
     */
    private static function processData(array $data): array
    {
        // 仅内部调用,不会生成路由
        return $data;
    }
}

生成的路由:

  • GET|POST /admin/loginAdminController::login()
  • GET /admin/dashboard?tab=overviewAdminController::dashboard()
  • processData()不会注册为路由(private 方法)

RouteMapping 配置

php
#[RouteMapping(
    paths: 'user/{id}',           // 路由路径,支持动态参数
    method: 'GET',              // HTTP 方法:GET|POST|PUT|DELETE|*
    middlewares: [],            // 方法级中间件
    patterns: []                // 路由参数约束
)]
public static function show(#[InjectGet] int $id): array
{
    return ['id' => $id];
}

支持的 HTTP 方法

  • 单个方法:'GET''POST''PUT''DELETE''PATCH'
  • 多个方法:['GET', 'POST']
  • 所有方法:'*'

动态路由参数

支持在路由路径中定义动态参数,使用 {参数名} 语法:

php
<?php
declare(strict_types=1);

namespace App\Controller;

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

#[AutoController]
class ArticleController
{
    /**
     * 必选参数
     * 路由:/article/show/123
     */
    #[RouteMapping('show/{id}')]
    public static function show(#[InjectGet] int $id): array
    {
        return ['article_id' => $id];
    }

    /**
     * 可选参数(带 ?)
     * 路由:/article/category/php 或 /article/category
     */
    #[RouteMapping('category/{slug?}')]
    public static function category(#[InjectGet] ?string $slug = null): array
    {
        return ['category' => $slug ?? 'all'];
    }
}

完整示例:RESTful API 控制器

php
<?php
declare(strict_types=1);

namespace App\Controller;

use App\Interface\UserInfo;
use App\Service\UserService;
use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\HttpServer\AutoInject\InjectPost;
use Viswoole\HttpServer\Contract\ResponseInterface;
use Viswoole\Router\Annotation\AutoController;
use Viswoole\Router\Annotation\RouteMapping;
use App\Response;

/**
 * 用户资源控制器
 * 实现 RESTful 风格的 CRUD 接口
 */
#[AutoController(
    prefix: 'api/v1',
    middlewares: [\App\Middleware\ApiLogMiddleware::class]
)]
class UserController
{
    /**
     * 获取用户列表
     *
     * GET /api/v1/user/list?page=1&size=10&keyword=test
     *
     * @param int $page 页码
     * @param int $size 每页数量
     * @param string|null $keyword 搜索关键词
     * @return array 分页数据
     */
    #[RouteMapping(method: 'GET')]
    public static function list(
        #[InjectGet] int $page = 1,
        #[InjectGet] int $size = 10,
        #[InjectGet] ?string $keyword = null
    ): array {
        return UserService::getList($page, $size, $keyword);
    }

    /**
     * 获取用户详情
     *
     * GET /api/v1/user/detail?id=100
     *
     * @param int $id 用户ID
     * @return array 用户信息
     */
    #[RouteMapping(method: 'GET')]
    public static function detail(#[InjectGet] int $id): array
    {
        return UserService::getDetail($id);
    }

    /**
     * 创建用户
     *
     * POST /api/v1/user/create
     * Body: { "name": "张三", "gender": "male" }
     *
     * @param UserInfo $userInfo 用户信息 DTO
     * @return array 创建结果
     */
    #[RouteMapping(method: 'POST')]
    public static function create(#[InjectPost] UserInfo $userInfo): array
    {
        $userId = UserService::create($userInfo);
        return ['id' => $userId, 'message' => '创建成功'];
    }

    /**
     * 更新用户信息
     *
     * PUT /api/v1/user/update
     * Body: { "id": 100, "name": "新名称" }
     *
     * @param UserInfo $userInfo 用户信息 DTO
     * @return array 更新结果
     */
    #[RouteMapping(method: 'PUT')]
    public static function update(#[InjectPost] UserInfo $userInfo): array
    {
        $result = UserService::update($userInfo);
        return ['success' => $result, 'message' => $result ? '更新成功' : '更新失败'];
    }

    /**
     * 删除用户
     *
     * DELETE /api/v1/user/delete?id=100
     *
     * @param int $id 用户ID
     * @return array 删除结果
     */
    #[RouteMapping(method: 'DELETE')]
    public static function delete(#[InjectPost] int $id): array
    {
        $result = UserService::delete($id);
        return ['success' => $result, 'message' => $result ? '删除成功' : '删除失败'];
    }

    /**
     * 导出用户数据(返回文件下载响应)
     *
     * GET /api/v1/user/export?format=csv
     *
     * @param string $format 导出格式
     * @param Response $response 响应对象
     * @return ResponseInterface 文件下载响应
     */
    #[RouteMapping(method: 'GET')]
    public static function export(
        #[InjectGet] string $format = 'csv',
        Response $response
    ): ResponseInterface {
        $filePath = UserService::export($format);
        return $response->sendfile($filePath);
    }
}

最佳实践

1. 命名规范

php
// ✅ 推荐:使用有意义的名称,复数形式表示集合
class UserController {}       // 用户相关
class OrderController {}      // 订单相关
class ProductCategoryController {} // 产品分类

// ❌ 避免:模糊或不规范的命名
class Ctrl1 {}
class Uc {}
class user_controller {}

2. 方法命名建议

操作类型推荐命名HTTP 方法
列表list, indexGET
详情detail, show, readGET
创建create, store, addPOST
更新update, edit, modifyPUT/PATCH
删除delete, remove, destroyDELETE
导出export, downloadGET
其他操作使用动词开头根据语义选择

3. 组织大型控制器

当控制器方法过多时(超过 10 个方法),考虑拆分:

text
Controller/
├── User/
│   ├── BasicController.php      # 基础信息
│   ├── ProfileController.php    # 个人资料
│   └── SecurityController.php   # 安全设置
├── Order/
│   ├── ListController.php       # 订单列表
│   └── DetailController.php     # 订单详情

4. 使用依赖注入

对于需要的服务类,通过构造函数或方法参数注入:

php
#[AutoController]
class ReportController
{
    /**
     * 通过方法参数注入服务
     */
    public static function generate(
        #[InjectPost] ReportDto $dto,
        ReportService $service  // 自动从容器注入
    ): ResponseInterface {
        return $service->generate($dto);
    }
}

常见陷阱

1. 忘记添加 public 修饰符

php
// ❌ 错误:默认方法不是 public,不会被注册
function index(): array
{
    return [];
}

// ✅ 正确:显式声明 public
public static function index(): array
{
    return [];
}

2. 路径大小写问题

URL 路径通常不区分大小写,但为了保持一致性,建议统一使用小写:

php
// ✅ 推荐
#[AutoController(prefix: 'api/v1')]

// ⚠️ 可能导致混淆
#[AutoController(prefix: 'API/V1')]

3. 循环依赖问题

避免在控制器中直接实例化其他控制器,应通过 Service 层协调:

php
// ❌ 错误:控制器之间耦合
public static function orderList(): array
{
    $userCtrl = new UserController();
    return $userCtrl->detail($this->userId);
}

// ✅ 正确:通过 Service 层
public static function orderList(): array
{
    return OrderService::getOrdersByUser($this->userId);
}