# router **Repository Path**: fiberphp/router ## Basic Information - **Project Name**: router - **Description**: 🧭 FiberPHP 路由组件 —— 支持 RESTful、路由分组、中间件、参数绑定,基于协程的高性能路由调度。 - **Primary Language**: PHP - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-22 - **Last Updated**: 2026-09-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # fiberphp/router 路由包 应用导航层,负责路由注册、匹配、调度、中间件管线与控制器参数注入。通过 `RequestHandlerInterface` 与 `fiberphp/http` 解耦——HTTP 层只管协议通信,路由层只管请求分发。 ## 环境要求 - PHP >= 8.3 - 依赖 `fiberphp/contract`、`fiberphp/container`、`fiberphp/config`、`fiberphp/console`、`fiberphp/event`、`fiberphp/support`、`fiberphp/http`、`psr/log` ## 架构 ``` fiberphp/http (物理通信层) └─ onMessage → container()->get(RequestHandlerInterface) └─ RouterProvider 绑定 → Router::handleRequest() ├─ Dispatcher (路由匹配:静态路径 O(1) + 变量路径正则) ├─ Pipeline (中间件管线:全局 → 控制器 → 路由) └─ Controller DI (反射注入:Request/标量/Enum/Model/类) ``` ### 路由定义策略 | 优先级 | 方式 | 适用场景 | |--------------|-------------------------------|--------------------------------------| | 一级(主力) | PHP 8 Attribute 注解 | 所有业务 API、Web 页面、Restful 资源 | | 二级(辅助) | 闭包/数组文件 `route/app.php` | 系统探针、健康检查、极简重定向 | ### 路由加载顺序 1. 路由缓存存在 → 直接反序列化 `runtime/cache/routes.php` 2. 无缓存 → `AttributeScanner` 扫描控制器注解 → 加载 `route/*.php` 闭包文件 ## 配置 路由包无需配置文件,默认行为代码内置: - 控制器扫描目录固定为 `App\Controller\`(约定,RouterProvider 内置) - 无注解控制器的默认回退路由默认开启(AttributeScanner 内置,可用 `#[Route\NoDefaultRoute]` 类级注解禁用单个控制器) 应用路由 `route/app.php`(闭包/数组回调,需安装本包): ```php use FiberPHP\Http\Response; use FiberPHP\Router\Router; Router::get('/ping', fn () => 'pong'); Router::get('/health', fn () => 'ok'); Router::get('/favicon.ico', fn () => new Response(204)); ``` > 业务路由请使用控制器注解(可缓存);闭包路由无法缓存,生产 `route:cache` 后此文件不加载。 ## 路由定义 ### 1. Attribute 注解(推荐) ```php namespace App\Controller; use FiberPHP\Router\Attribute as Route; use FiberPHP\Http\Request; use FiberPHP\Http\Response; class UserController { #[Route\Get('/users', name: 'users.index')] public function index(Request $request): Response { return json(['data' => []]); } #[Route\Get('/users/{id}', name: 'users.show')] public function show(int $id): Response { return json(['id' => $id]); } #[Route\Post('/users', name: 'users.store')] public function store(Request $request): Response { return json(['code' => 0], 201); } #[Route\Put('/users/{id}', name: 'users.update')] public function update(int $id, Request $request): Response { return json(['id' => $id]); } #[Route\Delete('/users/{id}', name: 'users.destroy')] public function destroy(int $id): Response { return json(['code' => 0]); } } ``` #### 可用的 HTTP 方法注解 | 注解 | HTTP 方法 | |--------------------|-----------| | `#[Route\Get]` | GET | | `#[Route\Post]` | POST | | `#[Route\Put]` | PUT | | `#[Route\Patch]` | PATCH | | `#[Route\Delete]` | DELETE | | `#[Route\Head]` | HEAD | | `#[Route\Options]` | OPTIONS | | `#[Route\Any]` | 以上全部 | #### Route 基类参数 所有 HTTP 方法注解继承 `Route` 抽象类,构造参数: ```php #[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] abstract class Route { public function __construct( public string $path = '', // 路由路径 public array $middleware = [], // 路由中间件 public string $name = '', // 路由名称 public array $where = [], // 参数正则约束 public bool $autoPrefix = true, // 是否自动拼接组前缀 ) {} } ``` 同一个方法可叠加多个注解(`IS_REPEATABLE`): ```php #[Route\Get('/users', name: 'users.index')] #[Route\Get('/users/list', name: 'users.list')] public function index(): Response { ... } ``` ### 2. 路由分组 `#[RouteGroup]` ```php #[Route\RouteGroup( prefix: '/api', middleware: ['auth'], namePrefix: 'api.', )] class UserController { // → GET /api/users, 名:api.users.index #[Route\Get('/users', name: 'users.index')] public function index() { ... } } ``` | 参数 | 说明 | |--------------|---------------------------------------------------| | `prefix` | 路径前缀,拼接到组内所有路由 | | `middleware` | 组级中间件,对组内所有路由生效 | | `namePrefix` | 名称前缀,拼接到组内所有路由名 | | `version` | 显式版本号覆盖(如 `'v2'`),替换自动推断的版本段 | | `autoPrefix` | 是否启用命名空间自动前缀推断(默认 true) | ### 3. 资源路由 `#[Resource]` ```php #[Route\Resource(name: 'posts', only: ['index', 'show', 'store', 'update', 'destroy'])] class PostController { public function index() { ... } public function show(int $id) { ... } public function store(Request $request) { ... } public function update(int $id, Request $request) { ... } public function destroy(int $id) { ... } } ``` 自动注册的路由: | HTTP 方法 | 路径 | 控制器方法 | 路由名称 | |-----------|------------------------|------------|------------------| | GET | `/posts` | `index` | `posts.index` | | GET | `/posts/create` | `create` | `posts.create` | | POST | `/posts` | `store` | `posts.store` | | GET | `/posts/{id}` | `show` | `posts.show` | | GET | `/posts/{id}/edit` | `edit` | `posts.edit` | | PUT | `/posts/{id}` | `update` | `posts.update` | | DELETE | `/posts/{id}` | `destroy` | `posts.destroy` | | PUT | `/posts/{id}/recovery` | `recovery` | `posts.recovery` | `only` 白名单控制只注册部分动作。不传 `only` 则根据控制器是否存在对应方法自动注册。 ### 4. 闭包路由(应用级) ```php // route/app.php use App\Controller\WebhookController; use FiberPHP\Router\Router; Router::get('/ping', fn () => 'pong'); Router::post('/webhook', [WebhookController::class, 'handle']); ``` | 方法 | 说明 | |--------------------------------------------------|------------------------| | `Router::get($path, $callback)` | 注册 GET 路由 | | `Router::post($path, $callback)` | 注册 POST 路由 | | `Router::put($path, $callback)` | 注册 PUT 路由 | | `Router::patch($path, $callback)` | 注册 PATCH 路由 | | `Router::delete($path, $callback)` | 注册 DELETE 路由 | | `Router::any($path, $callback)` | 匹配所有方法 | | `Router::add($methods, $path, $callback)` | 自定义方法(可传数组) | | `Router::group($path, $callback)` | 路由分组(支持嵌套) | | `Router::resource($name, $controller, $options)` | 资源路由 | | `Router::getByName($name)` | 按名称获取路由对象 | | `Router::getRoutes()` | 获取所有路由 | ## API 版本管理 采用"模块化 + 版本子目录"架构,从控制器命名空间自动推断版本前缀。 ### 自动推断规则 ``` App\Controller\V1\UserController → /v1/users 名称前缀: v1. App\Controller\V2\UserController → /v2/users 名称前缀: v2. App\Controller\V2\Admin\RoleController → /v2/admin/roles 名称前缀: v2.admin. ``` - `V\d+` 命名空间段识别为版本号,转为小写 `vN` 作为路径和名称前缀 - 其余段转为 snake_case 拼入路径 - 类名转为 snake_case 作为资源路径 ### 示例 ```php namespace App\Controller\V1; use FiberPHP\Router\Attribute as Route; // 自动推断:前缀 /v1, 名称前缀 v1. class UserController { #[Route\Get('/users', name: 'users.index')] // → GET /v1/users, 名:v1.users.index public function index() { ... } } ``` ```php namespace App\Controller\V2; use FiberPHP\Router\Attribute as Route; // 自动推断:前缀 /v2, 名称前缀 v2. class UserController { #[Route\Get('/users', name: 'users.index')] // → GET /v2/users, 名:v2.users.index public function index() { ... } } ``` ### 显式版本覆盖 ```php #[Route\RouteGroup(version: 'v3')] class UserController { // 即使类在 V1 命名空间下,路径也会是 /v3/users #[Route\Get('/users')] public function index() { ... } } ``` ## 默认回退路由 当控制器没有任何方法注解时,默认开启回退路由,自动将所有 public 方法注册为 `ANY` 路由: ```php namespace App\Controller; class IndexController { public function index() // → ANY /index { return 'hello'; } public function healthCheck() // → ANY /health_check { return 'ok'; } } ``` 方法名自动转为 snake_case 作为路径。使用 `#[Route\NoDefaultRoute]` 可禁止此行为: ```php #[Route\NoDefaultRoute] class ApiController { // 不会自动注册任何路由 public function internalMethod() { ... } } ``` ## 路由参数 ### 动态参数 ```php #[Route\Get('/users/{id}')] public function show(int $id) { ... } ``` ### 可选参数 ```php #[Route\Get('/posts/{id?}')] public function show(int $id = 1) { ... } ``` ### 正则约束 通过 `where` 参数约束参数格式: ```php #[Route\Get('/users/{id}', where: ['id' => '\d+'])] public function show(int $id) { ... } // 路径编译为 /users/{id:\d+} ``` ## 路由命名与 URL 生成 ```php #[Route\Get('/users/{id}', name: 'users.show')] public function show(int $id) { ... } // 通过名称获取路由 $route = Router::getByName('users.show'); // 生成 URL $url = $route->url(['id' => 42]); // /users/42 // 多余参数自动拼为查询字符串 $url = $route->url(['id' => 42, 'tab' => 'profile']); // /users/42?tab=profile ``` ## 中间件 执行顺序:全局中间件 → 控制器中间件 → 路由中间件 → 控制器方法。 ### 全局中间件 在 `config/http.php` 的 `middleware.global` 键注册: ```php 'middleware' => [ 'global' => [ \App\Middleware\CorsMiddleware::class, \App\Middleware\TraceMiddleware::class, ], 'aliases' => [ 'auth' => \App\Middleware\AuthMiddleware::class, ], ], ``` ### 中间件别名 `middleware.aliases` 将短名映射到中间件类,路由/控制器中间件可用别名替代完整类名: ```php // config/http.php 'middleware' => [ 'aliases' => [ 'auth' => \App\Middleware\AuthMiddleware::class, 'admin' => \App\Middleware\AdminMiddleware::class, ], ], // 路由注解用别名 #[Route\Get('/admin', middleware: ['auth', 'admin'])] ``` ### 控制器中间件 ```php class AdminController { protected static array $middleware = [ \App\Middleware\AuthMiddleware::class, ]; } ``` ### 路由中间件 通过注解的 `middleware` 参数指定: ```php #[Route\Get('/admin/dashboard', middleware: ['auth', 'admin'])] public function dashboard() { ... } ``` 通过 `#[RouteGroup]` 批量指定: ```php #[Route\RouteGroup(prefix: '/api', middleware: ['auth'])] class UserController { ... } ``` ### 中间件实现 中间件类需实现 `FiberPHP\Http\Contract\MiddlewareInterface`: ```php use FiberPHP\Http\Contract\MiddlewareInterface; use FiberPHP\Http\Request; use FiberPHP\Http\Response; class AuthMiddleware implements MiddlewareInterface { public function process(Request $request, callable $handler): Response { if (!$request->header('authorization')) { return new Response(401, [], 'Unauthorized'); } return $handler($request); } } ``` ## 控制器参数注入 基于反射的自动参数注入,首次请求分析方法签名并缓存元数据。 | 参数类型 | 注入方式 | |-------------------------------------|--------------------------------| | `Request`(或其父类) | 注入当前请求对象 | | 标量(int/float/bool/string/array) | 从请求参数获取并自动类型转换 | | `mixed`/`resource` | 直接透传请求参数值 | | 枚举(Enum) | 按名称或 backed 值匹配 | | `FiberPHP\Model` 子类 | 以请求数据为 `attributes` 构造 | | 其他类(有构造函数) | 递归解析构造参数,容器构造 | | 其他类(无构造参数) | 容器实例化 | ```php use FiberPHP\Http\Request; use App\Service\UserService; use App\Enum\UserStatus; class UserController { // Request + 路由参数 public function show(Request $request, int $id) { ... } // 标量 + 默认值 public function search(string $keyword, int $page = 1) { ... } // 依赖注入 public function create(UserService $service) { ... } // 枚举注入 public function status(UserStatus $status) { ... } } ``` 缺少必填参数或类型不匹配时抛 `FiberPHP\Http\Exception\HttpException`(HTTP 400,消息透传,出错参数名等细节随 context 仅在 debug 模式输出)。 ## 路由缓存 ```bash # 编译路由缓存(仅支持控制器动作,闭包不可缓存) php fiberphp route:cache # 清理路由缓存 php fiberphp route:clear ``` 缓存文件生成在 `runtime/cache/routes.php`,存在时直接反序列化加载,跳过注解扫描。 > `route/app.php` 闭包路由无法缓存,`route:cache` 会抛 `RuntimeException`。生产环境建议使用控制器动作。 ## 命令行工具 ```bash # 查看所有已注册路由 php fiberphp route:list ``` 输出表格包含 URI、HTTP 方法、回调、中间件、路由名称。 ## 与 fiberphp/http 的关系 ``` fiberphp/http fiberphp/router ┌─────────────────────┐ ┌─────────────────────────┐ │ TCP 连接管理 │ │ 控制器注解扫描 │ │ HTTP 协议解析 │ │ 路由匹配 (Dispatcher) │ │ 全局中间件管线 │ │ 控制器/路由中间件 │ │ 异常渲染 │ Request │ 控制器 DI 注入 │ │ 连接保活 │ ────────→ │ 版本前缀推断 │ │ │ Response │ 路由缓存 │ │ RequestHandlerIntf │←──────────│ Router::handleRequest() │ │ (Contract 层) │ 绑定 │ RouterProvider │ └─────────────────────┘ └─────────────────────────┘ ``` **解耦点**:`FiberPHP\Http\Contract\RequestHandlerInterface` - `fiberphp/http` 的 `Http::onMessage()` 通过容器获取 `RequestHandlerInterface` 实现 - `fiberphp/router` 的 `RouterProvider::register()` 绑定该接口到匿名类,委托 `Router::handleRequest()` - 若未安装 `fiberphp/router`,容器返回 null,回退到 `defaultHandler()` 直接抛 `NotFoundHttpException`(404) ## 目录结构 ``` src/ ├── Attribute/ # 路由注解 │ ├── Route.php # 抽象基类(path/middleware/name/where/autoPrefix) │ ├── Get.php # GET 注解 │ ├── Post.php # POST 注解 │ ├── Put.php # PUT 注解 │ ├── Patch.php # PATCH 注解 │ ├── Delete.php # DELETE 注解 │ ├── Head.php # HEAD 注解 │ ├── Options.php # OPTIONS 注解 │ ├── Any.php # 全方法注解 │ ├── RouteGroup.php # 类级分组(prefix/middleware/namePrefix/version) │ ├── Resource.php # 类级 RESTful 资源 │ └── NoDefaultRoute.php # 禁止默认回退路由 ├── Command/ # 控制台命令 │ ├── RouteList.php # route:list │ ├── RouteCache.php # route:cache │ └── RouteClear.php # route:clear ├── AttributeScanner.php # 控制器注解扫描 + 版本前缀推断 + 回退路由 ├── Dispatcher.php # 路由编译与匹配(静态 O(1) + 变量正则) ├── DispatchStatus.php # 调度状态枚举(Found/NotFound/MethodNotAllowed) ├── Route.php # 路由值对象(name/middleware/params/url) ├── Router.php # 路由注册入口 + 请求调度 + 中间件管线 + DI 注入 ├── RouterProvider.php # 服务提供者(绑定接口 + 加载路由) └── Install.php # 安装器(#[Package] 声明) ``` ## License [MIT](LICENSE)