简介
本技术文档聚焦 DouPHP 的“路由分组构建器”能力,围绕 RouteGroupBuilder 及其协作组件,系统阐述如何以声明式、链式 API 组织和管理相关路由集合。内容涵盖:
- 设计理念与使用场景:按模块+控制器维度聚合动作,统一前缀、命名空间、中间件等上下文。
- 嵌套分组与继承机制:通过 RouteGroupRegistrar 将 name 前缀与 compositeModule 标记入栈,组内 resource/group/单动词 builder 自动继承并叠加。
- 性能优势与最佳实践:批量展开、方法白名单、避免 permissive any()、合理拆分资源与分组。
- 复杂场景示例:多层嵌套分组、条件分组、动态分组策略。
- 调试技巧与常见问题:名称推导、前缀拼接、中间件生效顺序、404/405 定位。
- 与全局路由的交互:入口调度、解析器、中间件装配。
项目结构
DouPHP 的路由系统在构建期通过门面 Route 暴露 fluent API,实际工作由多个 Builder 与 DSL trait 协作完成:
- 门面与入口:Route::group()/resource()/name()->group() 等。
- 分组构建器:RouteGroupBuilder 负责同模块+控制器的多 action 显式枚举。
- 资源构建器:RouteResourceBuilder 提供 CRUD 标准集 + only/except + extras。
- 组级注册器:RouteGroupRegistrar 实现 group(callable) 属性入栈/出栈。
- 名称解析与中间件 DSL:RouteNameResolution、RouteMiddlewareDsl 被三个 builder 复用。
- 条目累积与分发:RouteCollector(由门面内部持有)、RouteEntry、各端 Resolver 与 DelegatingRouter。
graph TB
A["Route 门面"] --> B["RouteGroupBuilder"]
A --> C["RouteResourceBuilder"]
A --> D["RouteGroupRegistrar"]
B --> E["RouteNameResolution"]
B --> F["RouteMiddlewareDsl"]
C --> E
C --> F
D --> G["RouteCollector累积器"]
B --> G
C --> G
G --> H["RouteEntry 列表"]
H --> I["DelegatingRouter / 各端 Resolver"]
核心组件
- RouteGroupBuilder:声明式分组构建器,收集 prefix/sub/name/rootAction,按动词分桶 actions,析构时展开为 RouteEntry 推入 collector。
- RouteGroupRegistrar:实现 Laravel 风格的 Route::name('...')->group(callable),将 name 前缀与 compositeModule 标记入栈,执行回调后出栈。
- RouteNameResolution:处理 name 前缀叠加与 compositeModule 对 module 字段的影响;判定根动作是否取裸 nameBase。
- RouteMiddlewareDsl:提供 middleware/withoutMiddleware/skipAllMiddleware/auth/throttle/permission 等 DSL,汇总为 RouteEntry 字段。
- RouteResourceBuilder:CRUD 标准动作集 + only/except + 动词 extras,最终同样产出 declared RouteEntry。
- RouteEntryBuilder:单条路由的 fluent 构建器,与 group/resource 共同产出 RouteEntry。
- MiddlewareRegistry:组装默认中间件栈与路由级细化,支持 skip/without/append/params 四种语义。
- DelegatingRouter:三端 Router 的统一代理,透传 dispatch 并提供 current() 读取当前路由信息。
- ApiResolver:API 端解析器,基于 declared 条目进行 URL 命中与方法过滤,结合中间件装配返回 DispatchPlan。
架构总览
构建期:RouteManifestBuilder 安装 RouteCollector,include 各 route 文件期间,builder 链式调用生成 RouteEntry 并 push 到 collector。 运行期:DelegatingRouter 委托具体端 Router 进行 dispatch;各端 Resolver 根据 declared 条目匹配 URL 与方法,结合 MiddlewareRegistry 装配中间件链,返回响应或错误计划。
sequenceDiagram
participant R as "Route 门面"
participant RG as "RouteGroupRegistrar"
participant RB as "RouteGroupBuilder"
participant RC as "RouteCollector"
participant RE as "RouteEntry"
participant DR as "DelegatingRouter"
participant RES as "各端 Resolver"
participant MW as "MiddlewareRegistry"
Note over R,RC : 构建期
R->>RG : name('...')->group(callable)
RG->>RC : pushGroupAttributes(namePrefix, composite)
R->>RB : group(module, controller)->prefix(...)->get([...])
RB->>RC : push(RouteEntry)
RG->>RC : popGroupAttributes()
Note over DR,RES : 运行期
DR->>RES : dispatch()
RES->>RES : 匹配 URL + HTTP 方法
RES->>MW : compose(默认栈 + 路由级细化)
MW-->>RES : 中间件实例链
RES-->>DR : Response/DispatchPlan
详细组件分析
RouteGroupBuilder 设计要点
- 职责:维护 module/controller/prefix/sub/rootActionOverride,按动词方法追加 actions 到 buckets,析构时 flush 展开为 RouteEntry。
- 规则:
- root 动作默认 index,可经 root() 覆盖;root 动作 pattern=prefix,非 root 为 prefix/action。
- nameBase 默认主组为 module,子组为 module.sub;可通过 name() 覆盖或叠加前缀。
- methods 严格白名单:get/post/put/patch/delete;any() 为 permissive 逃生口。
- 中间件:通过 RouteMiddlewareDsl 汇总 middleware/withoutMiddleware/middleware_params/skip_all_middleware。
- 名称解析:通过 RouteNameResolution 计算最终 name 与 emitted module(compositeModule)。
classDiagram
class RouteGroupBuilder {
-collector
-module
-controller
-prefix
-sub
-rootActionOverride
-buckets
-registered
+prefix(prefix) self
+sub(sub) self
+name(name) self
+compositeModule() self
+root(action) self
+get(actions) self
+post(actions) self
+put(actions) self
+patch(actions) self
+delete(actions) self
+any(actions) self
-addBucket(methods, actions) self
-flush() void
}
RouteGroupBuilder ..|> RouteMiddlewareDsl
RouteGroupBuilder ..|> RouteNameResolution
分组属性继承(RouteGroupRegistrar + RouteNameResolution)
- 通过 Route::name('api.')->group(callable) 将 name 前缀与 compositeModule 标记入栈。
- 组内 resource/group/单 verb builder 在 register 时读取栈顶属性:
- name 前缀叠加:qualifyName(stackPrefix + arg, defaultLocalBase)。
- compositeModule:当 sub 非空时,emit 的 module 字段变为 module_sub。
- 保证幂等:若 base 已包含 prefix,不再重复叠加。
flowchart TD
S["进入 group(callable)"] --> P["pushGroupAttributes(namePrefix, composite)"]
P --> E["执行回调组内声明路由"]
E --> N{"name 参数尾点?"}
N -- 是 --> Q["qualifyName(stackPrefix + 'api.', defaultLocalBase)"]
N -- 否 --> Q2["qualifyName(stackPrefix, full name)"]
Q --> M["compositeModule? sub非空? -> emit module = module_sub"]
Q2 --> M
M --> X["popGroupAttributes()"]
中间件 DSL 与装配
- 支持四种语义:skipAllMiddleware(最高优先级)、withoutMiddleware(过滤默认栈)、middleware(追加别名/参数)、middlewareParams(覆盖默认中间件参数)。
- 各 builder 通过 middlewareFields() 汇总字段写入 RouteEntry,运行时由 MiddlewareRegistry 组装最终链。
flowchart TD
A["RouteEntry 携带字段"] --> B["MiddlewareRegistry.compose"]
B --> C{"skipAllMiddleware?"}
C -- 是 --> D["返回空链"]
C -- 否 --> E{"withoutMiddleware 过滤"}
E --> F["默认栈末尾追加 middleware"]
F --> G{"middlewareParams 覆盖"}
G --> H["生成最终中间件实例链"]
与全局路由的交互
- 构建期:Route 门面仅在 manifest 构建期使用,通过 useCollector(collector) 安装累积器,include 完成后卸载,避免状态外泄。
- 运行期:DelegatingRouter 统一代理三端 Router;各端 Resolver 依据 declared 条目匹配 URL 与方法,结合中间件装配返回响应。
- API 端:ApiResolver 基于 declared 条目进行匹配与方法过滤,未匹配返回 404 JSON,方法不被接受返回 405 JSON。
sequenceDiagram
participant RM as "RouteManifestBuilder"
participant RF as "Route 门面"
participant RC as "RouteCollector"
participant DR as "DelegatingRouter"
participant AR as "ApiResolver"
RM->>RF : useCollector(collector)
RM->>RF : include route files (group/resource/verb)
RF->>RC : push(RouteEntry)
RM->>RF : useCollector(null)
DR->>AR : dispatch()
AR->>AR : 匹配URL+方法
AR-->>DR : Response/DispatchPlan
依赖关系分析
- RouteGroupBuilder 依赖:
- RouteCollector(累积器)用于 push RouteEntry。
- RouteNameResolution 用于名称推导与 module 字段解析。
- RouteMiddlewareDsl 用于中间件字段汇总。
- RouteGroupRegistrar 依赖:
- RouteCollector 的 pushGroupAttributes/popGroupAttributes 实现属性栈。
- 运行期依赖:
- DelegatingRouter 委托具体 Router。
- 各端 Resolver 与 MiddlewareRegistry 组合装配中间件。
graph LR
RGB["RouteGroupBuilder"] --> RC["RouteCollector"]
RGB --> RNR["RouteNameResolution"]
RGB --> RMD["RouteMiddlewareDsl"]
RGR["RouteGroupRegistrar"] --> RC
DR["DelegatingRouter"] --> RES["各端 Resolver"]
RES --> MW["MiddlewareRegistry"]
性能考量
- 批量展开:RouteGroupBuilder 在析构时一次性展开 buckets 为 RouteEntry,减少多次分配与判断。
- 方法白名单:优先使用 get/post/put/patch/delete 明确方法,避免 any() 导致的不必要匹配开销。
- 合理分组:将同模块+控制器的动作集中到一个 group,减少分散声明带来的匹配复杂度。
- 名称推导优化:利用 name() 前缀与 compositeModule 减少重复命名逻辑,降低后续权限/菜单/审计的解析成本。
- 中间件最小化:仅对需要鉴权/限流的分组启用相应中间件,避免全量链路负担。
故障排查指南
- 404 未匹配:
- 检查 prefix 是否正确,确保 group 中调用了 prefix()。
- 确认 action 是否在对应动词方法中被声明(如 GET 页面需使用 get([...]))。
- 查看 RouteEntry 的 pattern 与 methods 是否符合预期。
- 405 方法不允许:
- 确认请求方法与声明的 methods 一致;RESTful update 使用 put(['...']) 同时接受 PUT/PATCH。
- 名称不正确:
- 检查 name() 是否以点结尾(前缀模式)或全名覆盖;确认 stackPrefix 与 defaultLocalBase 叠加结果。
- 子控制器组需配合 sub() 与 compositeModule() 使 routeModule 正确。
- 中间件未生效:
- 确认 middleware/withoutMiddleware/middlewareParams 配置正确;必要时使用 skipAllMiddleware() 验证链路。
- 检查各端 Resolver 的默认中间件栈与别名映射。
结论
RouteGroupBuilder 提供了清晰、可维护的分组路由构建方式,结合 RouteGroupRegistrar 的组级属性继承、RouteNameResolution 的名称推导与 RouteMiddlewareDsl 的中间件 DSL,能够在大型项目中有效组织与管理路由集合。遵循方法白名单、合理分组与最小中间件原则,可获得更好的可维护性与性能表现。
附录:示例与最佳实践
-
基础分组(前台/后台均可)
- 使用 Route::group('module', Controller::class)->prefix('...')->sub('...')->get([...]) 组织同模块+控制器的动作。
- 参考路径:ai.php(admin):38-88、chat.php(admin):37-92
-
带 name 前缀与 compositeModule 的分组
- 使用 Route::name('admin.')->compositeModule()->group(function(){...}) 统一命名与 routeModule。
- 参考路径:ai.php(admin):38-88、chat.php(admin):37-92
-
资源型分组(CRUD)
- 使用 Route::resource('module', Controller::class)->only([...])->get([...])->post([...]) 快速生成标准动作。
- 参考路径:ai.php(admin):38-88、chat.php(admin):37-92
-
条件分组与动态分组
- 在 group(callable) 中根据运行时条件决定是否注册某些子分组或动作(例如功能开关、模块卸载)。
- 注意:分组属性栈在 group 结束时弹出,确保异常路径也正确清理。
- 参考路径:RouteGroupRegistrar.php:81-100
-
中间件最佳实践
- 对敏感接口使用 throttle/auth/permission 参数糖;对公开接口使用 withoutMiddleware 豁免。
- 参考路径:RouteMiddlewareDsl.php:46-143、MiddlewareRegistry.php:24-47
-
与全局路由交互
- 构建期通过 Route::useCollector 安装/卸载累积器;运行期通过 DelegatingRouter 与 Resolver 完成匹配与分发。
- 参考路径:Route.php:21-67、DelegatingRouter.php:24-86、ApiResolver.php:30-62
-
常见陷阱
- 忘记调用 prefix():会在 flush 时抛出 LogicException。
- 误用 any():应尽量避免,除非确需任意方法命中同一控制器。
- 名称前缀重复叠加:qualifyName 会幂等处理,但仍建议保持命名规范。
- 参考路径:RouteGroupBuilder.php:269-321