路由分组
路由分组允许将一组具有相同前缀、中间件或其他公共属性的路由组织在一起,避免重复声明。通过 Router::group() 实现。
注意:
Router::group()第三个参数$id为必填项,用于唯一标识一个分组。
路径合并规则
子路由路径是否以 / 开头,决定了它与父级(分组)前缀的合并方式:
| 子路由写法 | 语义 | 合并结果(父级前缀 /api/v1) |
|---|---|---|
user/info | 相对路径 | /api/v1/user/info(拼接父前缀) |
/user/info | 绝对路径 | /user/info(忽略父前缀) |
/(单独斜杠) | 父级本身 | /api/v1(分组默认入口) |
- 相对路径(不以
/开头):与父级前缀拼接为完整路径,是最常用的写法; - 绝对路径(以
/开头):忽略所有父级前缀,直接作为根路由注册。适合在分组内声明一条脱离前缀的路由(如健康检查、首页跳转),而无需拆出分组; - 顶层路由(未处于任何分组内)没有父级前缀,
/user与user等价,均注册为/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'];
});