路由分组

路由分组允许将一组具有相同前缀、中间件或其他公共属性的路由组织在一起,避免重复声明。通过 Router::group() 实现。

注意Router::group() 第三个参数 $id 为必填项,用于唯一标识一个分组。

路径合并规则

子路由路径是否以 / 开头,决定了它与父级(分组)前缀的合并方式:

子路由写法语义合并结果(父级前缀 /api/v1
user/info相对路径/api/v1/user/info(拼接父前缀)
/user/info绝对路径/user/info忽略父前缀
/(单独斜杠)父级本身/api/v1(分组默认入口)
  • 相对路径(不以 / 开头):与父级前缀拼接为完整路径,是最常用的写法;
  • 绝对路径(以 / 开头):忽略所有父级前缀,直接作为根路由注册。适合在分组内声明一条脱离前缀的路由(如健康检查、首页跳转),而无需拆出分组;
  • 顶层路由(未处于任何分组内)没有父级前缀,/useruser 等价,均注册为 /user
  • 该规则同样适用于注解路由:#[RouteMapping]paths 相对于 #[Controller]prefix 合并(详见注解路由)。

基本分组

前缀分组

最常见的用途是为一批路由添加统一的前缀路径。分组内子路由使用相对路径(不以 / 开头):

php
use Viswoole\Router\Facade\Router;

Router::group('/api/v1', function () {
    Router::get('user/info', [UserController::class, 'info']);
    Router::post('user/create', [UserController::class, 'create']);
    Router::put('user/update', [UserController::class, 'update']);
    Router::delete('user/delete', [UserController::class, 'delete']);
}, 'api.v1');

// 实际路由:
// GET    /api/v1/user/info
// POST   /api/v1/user/create
// PUT    /api/v1/user/update
// DELETE /api/v1/user/delete

绝对路径子路由

若子路由以 / 开头,将忽略分组前缀直接作为根路由:

php
Router::group('/api/v1', function () {
    // 相对路径:继承分组前缀
    Router::get('user/info', [UserController::class, 'info']);

    // 绝对路径:忽略分组前缀,直接注册为根路由 /health
    Router::get('/health', [HealthController::class, 'check']);

    // 单独的 / 表示分组本身的路径 /api/v1
    Router::get('/', [UserController::class, 'index']);
}, 'api.v1');

// 实际路由:
// GET /api/v1/user/info
// GET /health
// GET /api/v1

分组中间件

为整个分组的所有路由统一添加中间件:

php
Router::group('/api/v1', function () {
    Router::get('order/list', [OrderController::class, 'list']);
    Router::get('order/{id}', [OrderController::class, 'detail']);
}, 'api.v1.order')->setMiddlewares([AuthMiddleware::class, RateLimitMiddleware::class]);

域名分组

将一组路由限定在特定域名下:

php
Router::group('/api', function () {
    Router::get('version', [ApiController::class, 'version']);
    Router::get('status', [ApiController::class, 'status']);
}, 'api')->setDomain('api.example.com');

// 仅在 api.example.com 域名下生效

嵌套分组

分组支持多层嵌套,内层继承外层的所有属性。嵌套分组的前缀同样遵循路径合并规则:不以 / 开头时与外层前缀拼接:

php
Router::group('/api', function () {

    // 'v1' 为相对路径,与外层 /api 拼接为 /api/v1
    Router::group('v1', function () {

        // 'user' 为相对路径,与 /api/v1 拼接为 /api/v1/user
        Router::group('user', function () {
            Router::get('info', [UserController::class, 'info']);
            Router::post('create', [UserController::class, 'create']);
        }, 'api.v1.user')->setMiddlewares([UserMiddleware::class]);

        Router::group('order', function () {
            Router::get('list', [OrderController::class, 'list']);
            Router::post('create', [OrderController::class, 'create']);
        }, 'api.v1.order')->setMiddlewares([OrderMiddleware::class]);

    }, 'api.v1')->setMiddlewares([ApiMiddleware::class, AuthMiddleware::class]);

}, 'api')->setDomain('api.example.com');

嵌套结果:

  • GET /api/v1/user/info — 中间件: [ApiMiddleware, AuthMiddleware, UserMiddleware]
  • POST /api/v1/order/create — 中间件: [ApiMiddleware, AuthMiddleware, OrderMiddleware]

注意:嵌套分组若以 / 开头(如 Router::group('/v2', ...) 嵌套在 /api 内),该分组会忽略外层前缀,直接作为根分组 /v2 注册——但它仍会继承外层的中间件、域名等属性。

分组属性汇总

Router::group() 返回的路由分组对象支持链式调用以下方法:

方法说明示例
setMiddlewares(array)添加中间件->setMiddlewares([Auth::class])
setDomain(string)限制域名->setDomain('api.example.com')
setSuffix(string)后缀要求->setSuffix('html')
setPatterns(array)参数正则约束->setPatterns(['id' => '\d+'])

与注解分组对应

配置文件的 Router::group() 与注解的 #[Controller] 是同一概念的两种表达方式,子路径合并规则也完全一致:

php
// 方式一:配置文件分组
Router::group('/api/v1', function () {
    Router::get('user/info', [UserController::class, 'info']);
}, 'api.v1')->setMiddlewares([AuthMiddleware::class]);

// 方式二:注解分组(效果等价)
#[Controller(prefix: '/api/v1', middlewares: [AuthMiddleware::class])]
class UserController
{
    // paths 不以 / 开头,与类前缀拼接为 /api/v1/user/info
    #[RouteMapping(paths: ['user/info'], method: ['GET'])]
    public function info(): array {}
}

选择建议:

  • 配置文件:跨控制器聚合、全局兜底路由
  • 注解:单控制器内部路由、与代码同位置维护

实际项目示例

php
<?php
// config/route/route.php
use App\Controller\Api\V1\{
    UserController,
    OrderController,
    ProductController,
};
use App\Controller\Admin\DashboardController;
use App\Middleware\{
    AuthMiddleware,
    AdminMiddleware,
    CorsMiddleware,
    RateLimitMiddleware,
};
use Viswoole\Router\Facade\Router;

/* ========== API 路由组 ========== */
Router::group('/api/v1', function () {

    // 用户模块
    Router::group('user', function () {
        Router::get('info', [UserController::class, 'info']);
        Router::post('login', [UserController::class, 'login']); // 无需认证
        Router::put('password', [UserController::class, 'updatePassword']);
    }, 'api.v1.user');

    // 订单模块(需认证)
    Router::group('order', function () {
        Router::get('list', [OrderController::class, 'list']);
        Router::get('{id}', [OrderController::class, 'detail'])
            ->setPatterns(['id' => '\d+']);
        Router::post('create', [OrderController::class, 'create']);
    }, 'api.v1.order')->setMiddlewares([AuthMiddleware::class]);

    // 商品模块(需认证 + 限流)
    Router::group('product', function () {
        Router::get('list', [ProductController::class, 'list']);
        Router::get('{id}', [ProductController::class, 'detail'])
            ->setPatterns(['id' => '\d+']);
    }, 'api.v1.product')->setMiddlewares([AuthMiddleware::class, RateLimitMiddleware::class]);

}, 'api.v1')->setMiddlewares([CorsMiddleware::class]);

/* ========== 后台管理路由组 ========== */
Router::group('/admin', function () {
    Router::get('dashboard', [DashboardController::class, 'index']);
    Router::get('settings', [DashboardController::class, 'settings']);
}, 'admin')->setMiddlewares([AdminMiddleware::class])->setDomain('admin.example.com');

/* ========== 兜底路由 ========== */
Router::miss(function () {
    return ['code' => 404, 'message' => 'Not Found'];
});