简介
本文面向 DouPHP 框架的"核心路由架构",围绕委托路由器模式展开,解释 DelegatingRouter 如何作为前台、后台、API 三端路由的统一抽象层;阐述路由状态管理机制(module、action、sub 的解析与存储);说明容器化依赖注入在路由系统中的应用;描述 Request 对象的路由信息获取机制;并覆盖路由生命周期管理、错误处理策略与性能优化方案。文末提供具体代码路径示例,展示路由系统的核心用法与扩展点。
更新 本次更新重点增强了路由条目的导航元数据能力,通过 RouteEntry::$nav 字段和 Request::routeEntry() 方法提供了完整的后台导航高亮支持,同时集成了 RouteNavDsl trait 到所有路由构建器中,实现了声明式的导航配置 API。
项目结构
DouPHP 采用"三端独立入口 + 统一路由抽象"的结构:
- 三端各自拥有薄壳 Router:负责从请求中读取 routeString,调用对应 Resolver 生成 DispatchPlan,再交由中央 Dispatcher 执行。
- 三端 Resolver:将 URL 映射为控制器 FQCN、方法名、参数与中间件链,并将 module/action/sub 等路由状态写入 Request。
- 中央 Dispatcher:在中间件管道内懒实例化控制器并调用方法,不关心具体业务。
- 委托路由器 DelegatingRouter:对外暴露统一的 dispatch() 与 current(),屏蔽三端差异,集中承载当前路由状态。
graph TB
A["入口 index.php"] --> B["DelegatingRouter::dispatch()"]
B --> C["前端 Router::dispatch()"]
B --> D["后台 Router::dispatch()"]
B --> E["API Router::dispatch()"]
C --> F["FrontResolver::resolve()"]
D --> G["AdminResolver::resolve()"]
E --> H["ApiResolver::resolve()"]
F --> I["Dispatcher::run()"]
G --> I
H --> I
I --> J["中间件管道 + 控制器调用"]
J --> K["Request::routeEntry() 访问路由条目"]
K --> L["RouteEntry::$nav 导航元数据"]
核心组件
- 委托路由器 DelegatingRouter:统一代理三端 Router,并提供 current() 聚合当前路由状态。
- 三端 Router:前端/后台/API 各自的调度器,仅做轻量编排。
- 三端 Resolver:将 URL 解析为 DispatchPlan(包含 fqcn、method、params、middlewares)。
- 中央 Dispatcher:在中间件管道中执行控制器方法。
- Request:承载路由状态(module/action/sub/routeString/routeLangSign)与路由参数,新增 routeEntry() 方法访问匹配的路由条目。
- RouteEntry:路由清单条目值对象,包含完整的路由元数据和导航信息,新增 $nav 字段支持后台导航高亮。
- RouteNavDsl:声明式路由导航元数据 fluent DSL,被 RouteEntryBuilder / RouteResourceBuilder / RouteGroupBuilder 复用。
更新 新增了 RouteEntry::$nav 字段和 Request::routeEntry() 方法,以及 RouteNavDsl trait 的完整集成,提供了更丰富的导航元数据访问能力和声明式配置支持。
架构总览
委托路由器模式的核心在于:
- 入口通过静态门面 Route 调用 DelegatingRouter,避免在三端入口中硬编码具体 Router。
- DelegatingRouter 持有 delegate(前端/后台/API Router),dispatch() 透传调用,current() 从容器中读取 Request 以返回统一的路由状态数组。
- 各端 Resolver 负责"声明式匹配 + 安全默认栈 + 模块开关校验 + 视图/主题装配",最终产出 DispatchPlan。
- Dispatcher 仅在中间件管道中执行控制器,不关心 404/405 渲染,交由各端 Router 决定响应形态。
- 新增 Request::routeEntry() 提供对匹配路由条目的直接访问,支持在控制器中获取完整的路由元数据,包括导航信息。
sequenceDiagram
participant 客户端 as "客户端"
participant 委托 as "DelegatingRouter"
participant 端路由 as "三端 Router"
participant 解析器 as "Resolver"
participant 分发器 as "Dispatcher"
participant 管道 as "中间件管道"
participant 控制器 as "控制器"
participant 请求 as "Request"
participant 路由条目 as "RouteEntry"
客户端->>委托 : 调用 dispatch()
委托->>端路由 : 透传 dispatch()
端路由->>解析器 : resolve(request, container)
解析器-->>端路由 : DispatchPlan(fqcn, method, params, middlewares)
端路由->>分发器 : run(plan, container)
分发器->>管道 : 执行中间件链
管道->>控制器 : 懒实例化并调用方法
控制器->>请求 : request()->routeEntry()
请求-->>控制器 : RouteEntry 或 null
控制器->>路由条目 : 访问 $nav 导航元数据
控制器-->>分发器 : Response 或 null
分发器-->>端路由 : 返回值
端路由-->>客户端 : 发送响应或继续流程
详细组件分析
委托路由器 DelegatingRouter:三端统一抽象层
- 职责:
- setDelegate() 注入具体 Router(前端/后台/API)。
- dispatch() 透传 delegate->dispatch()。
- current() 从容器中读取 Request,聚合 module/action/sub/route/lang/is_home。
- 设计取舍:
- 不暴露注册类方法(get/post/middleware/group/name),因为 DouPHP 采用约定式路由,注册口在各端 init/route.php 与 *RouteRules。
- 三端 Router 作为 delegate,保持 DelegatingRouter 极简。
classDiagram
class DelegatingRouter {
-delegate
+setDelegate(delegate)
+dispatch() mixed
+current() array
}
前端路由:FrontResolver + Router
- 入口行为:
- 读取 routeString(已剥语言前缀),调用 FrontResolver::resolve()。
- 未命中时渲染 page_wrong 提示;命中后交给 Dispatcher::run()。
- 解析逻辑:
- 通过 PrettyRouteMatcher 匹配 declared 条目,得到 module/action/sub/controller。
- 校验会员模块可用性(Module::assertUserAvailable)。
- 组装中间件链(安全头、可信代理、限流、可选用户认证、CSRF)。
- 设置 Request 路由状态与参数,并装配表单目标与主题扩展。
- 404 处理:
- 未命中或未找到控制器时返回 notFound,Router 渲染 page_wrong。
flowchart TD
Start(["前端 dispatch"]) --> ReadReq["读取 routeString"]
ReadReq --> Match["PrettyRouteMatcher::normalize"]
Match --> |未匹配| NotFound["DispatchPlan::notFound()"]
Match --> |匹配| Validate["模块可用性校验"]
Validate --> ComposeMW["组装中间件链"]
ComposeMW --> SetRoute["Request::setRoute/setRouteParams"]
SetRoute --> Plan["构造 DispatchPlan"]
Plan --> Run["Dispatcher::run()"]
NotFound --> Render["渲染 page_wrong"]
后台路由:AdminResolver + Router
- 入口行为:
- 读取 routeString,调用 AdminResolver::resolve()。
- 未命中或方法不允许时重定向到 admin.index 并携带错误提示;命中后交给 Dispatcher::run()。
- 解析逻辑:
- 通过 BackendDeclaredMatcher 按 admin/route/*.php 中的 declared 条目匹配。
- 组装中间件链(安全头、可信代理、认证、权限、CSRF、工作区)。
- 设置 Request 路由状态与参数,并装配表单目标。
- 404/405 处理:
- 未命中返回 notFound;方法不被接受返回 methodNotAllowed,Router 重定向并带 Allow 头。
flowchart TD
StartA(["后台 dispatch"]) --> ReadReqA["读取 routeString"]
ReadReqA --> MatchA["BackendDeclaredMatcher::match"]
MatchA --> |未匹配| NotFoundA["DispatchPlan::notFound()"]
MatchA --> |方法不允许| MethodNotAllowedA["DispatchPlan::methodNotAllowed()"]
MatchA --> |匹配| ComposeMWA["组装中间件链"]
ComposeMWA --> SetRouteA["Request::setRoute/setRouteParams"]
SetRouteA --> PlanA["构造 DispatchPlan"]
PlanA --> RunA["Dispatcher::run()"]
NotFoundA --> RedirectA["重定向 admin.index + 错误提示"]
MethodNotAllowedA --> RedirectA
API 路由:ApiResolver + Router
- 入口行为:
- 读取 routeString,调用 ApiResolver::resolve()。
- 未命中或方法不允许时返回 JSON 404/405;命中后交给 Dispatcher::run()。
- 解析逻辑:
- 通过 BackendDeclaredMatcher 按 api/route/*.php 中的 declared 条目匹配。
- 组装中间件链(安全头、可信代理、限流、可选用户认证)。
- 设置 Request 路由状态与参数。
- 404/405 处理:
- 未命中返回 NOT_FOUND 404 JSON;方法不被允许返回 NOT_FOUND 405 JSON,并附带 allow 列表。
flowchart TD
StartB(["API dispatch"]) --> ReadReqB["读取 routeString"]
ReadReqB --> MatchB["BackendDeclaredMatcher::match"]
MatchB --> |未匹配| NotFoundB["DispatchPlan::notFound()"]
MatchB --> |方法不允许| MethodNotAllowedB["DispatchPlan::methodNotAllowed()"]
MatchB --> |匹配| ComposeMWB["组装中间件链"]
ComposeMWB --> SetRouteB["Request::setRoute/setRouteParams"]
SetRouteB --> PlanB["构造 DispatchPlan"]
PlanB --> RunB["Dispatcher::run()"]
NotFoundB --> Json404["返回 404 JSON"]
MethodNotAllowedB --> Json405["返回 405 JSON + allow"]
中央分发器 Dispatcher:中间件管道与控制器执行
- 职责:
- 接收 DispatchPlan,若 notFound 直接返回 null,交由各端 Router 渲染。
- 在中间件管道之前注入 Request 路由参数,确保中间件与控制器均可通过 Request::route() 获取。
- 懒实例化控制器并通过容器调用方法,返回控制器返回值(常为 Response 或 null)。
- 关键点:
- 不做路由决策、不做 method_exists 兜底、不渲染 HTTP 响应。
- 未匹配时返回 null,交由各端 Router 渲染端专属 404。
flowchart TD
StartD(["Dispatcher::run"]) --> CheckNotFound{"plan.isNotFound()?"}
CheckNotFound --> |是| ReturnNull["返回 null"]
CheckNotFound --> |否| InjectParams["Request::setRouteParams(params)"]
InjectParams --> MWRun["MiddlewarePipeline::run(...)"]
MWRun --> MakeCtrl["Container::make(fqcn)"]
MakeCtrl --> CallMethod["Container::call(controller, method, ['__scene' => method])"]
CallMethod --> ReturnResult["返回控制器返回值"]
新增:Request::routeEntry() 路由条目访问
- 功能:提供对匹配路由条目的直接访问,返回 RouteEntry 对象或 null。
- 用途:在控制器中获取完整的路由元数据,包括 pattern、controller、action、nav 等字段。
- 实现:简单 getter 方法,返回 $this->routeEntry 属性。
classDiagram
class Request {
+routeEntry() RouteEntry|null
+route($key, $default) mixed
+setRouteParams(array $params) void
}
class RouteEntry {
+name string|null
+pattern string
+controller string|null
+action string|null
+nav array
+acceptsMethod(string) bool
}
Request --> RouteEntry : 返回匹配的路由条目
新增:RouteEntry::$nav 导航元数据字段
- 功能:存储后台导航元数据(cur/group/sub_cur),支持标量值和 action 级映射。
- 用途:为后台导航高亮提供数据支持,空数组表示未声明,由消费端走约定兜底。
- 特性:在构造函数中初始化,toArray() 方法中包含该字段。
classDiagram
class RouteEntry {
+name string|null
+pattern string
+controller string|null
+action string|null
+sub string|null
+middleware array
+methods array
+nav array
+is_short_url_aware bool
+is_family bool
+source string
+acceptsMethod(httpMethod) bool
+toArray() array
}
新增:RouteNavDsl 导航元数据 DSL
- 功能:声明式路由导航元数据 fluent DSL,被 RouteEntryBuilder / RouteResourceBuilder / RouteGroupBuilder 复用。
- 用途:在路由定义中声明后台导航高亮归属,如
Route::resource('user')->sub('log')->nav(array('cur' => 'user'))->get(['index' => ...]); - 特性:支持标量值和 action 级映射,未声明的键由消费端走约定兜底。
classDiagram
class RouteNavDsl {
-navMeta array|null
+nav(array) self
+navFields() array
}
class RouteEntryBuilder {
use RouteNavDsl
+register() void
}
class RouteGroupBuilder {
use RouteNavDsl
+flush() void
}
class RouteResourceBuilder {
use RouteNavDsl
+pushEntry() void
}
RouteNavDsl <|-- RouteEntryBuilder
RouteNavDsl <|-- RouteGroupBuilder
RouteNavDsl <|-- RouteResourceBuilder
依赖关系分析
- DelegatingRouter 依赖 Container 与 Request,用于 current() 读取路由状态。
- 三端 Router 依赖各自 Resolver 与中央 Dispatcher。
- Resolver 依赖匹配器(PrettyRouteMatcher/BackendDeclaredMatcher)、中间件注册表、模块开关、视图与主题扩展。
- Dispatcher 依赖中间件管道与容器,负责控制器懒实例化与方法调用。
- 新增 Request 现在可以访问 RouteEntry 对象,提供完整的路由元数据。
- 新增 RouteEntryBuilder / RouteGroupBuilder / RouteResourceBuilder 都使用 RouteNavDsl trait,支持导航元数据声明。
graph LR
DR["DelegatingRouter"] --> R1["前端 Router"]
DR --> R2["后台 Router"]
DR --> R3["API Router"]
R1 --> FR["FrontResolver"]
R2 --> AR["AdminResolver"]
R3 --> APR["ApiResolver"]
FR --> DP["Dispatcher"]
AR --> DP
APR --> DP
DP --> MP["中间件管道"]
DP --> C["容器(控制器)"]
C --> RE["Request::routeEntry()"]
RE --> RTE["RouteEntry"]
RTE --> NAV["$nav 导航元数据"]
REB["RouteEntryBuilder"] --> RND["RouteNavDsl"]
RGB["RouteGroupBuilder"] --> RND
RRB["RouteResourceBuilder"] --> RND
性能考量
- 懒实例化控制器:Dispatcher 在中间件管道内才实例化控制器,减少无谓开销。
- 声明式路由匹配:Resolver 基于 declared 条目进行匹配,避免运行时反射全量扫描。
- 中间件按需组合:通过 MiddlewareRegistry 根据配置与路由级细化组装中间件链,避免不必要的中间件执行。
- 模块开关短路:Module::assertUserAvailable 在解析阶段关闭无关模块,防止容器反射到未启用模块。
- 路由参数集中注入:在 Dispatcher 中一次性注入 Request 路由参数,避免重复解析。
- 新增 RouteEntry 作为值对象,提供只读访问接口,避免额外的查询开销。
- 新增 RouteNavDsl 在构建期收集导航元数据,运行时零开销访问。
故障排查指南
- 前台未匹配:
- 现象:page_wrong 提示。
- 检查:FrontResolver 日志记录 unmatched 与 dispatch failed;确认 routeString 是否已剥语言前缀;确认 PrettyRouteMatcher 命中条目是否存在。
- 参考路径:FrontResolver.php:58-86、前端 Router.php:46-48。
- 后台未匹配或方法不允许:
- 现象:重定向到 admin.index 并携带错误提示;405 时返回 Allow 头。
- 检查:BackendDeclaredMatcher 匹配结果;HTTP 方法与 declared 条目是否一致;中间件是否拦截。
- 参考路径:后台 Router.php:47-56、AdminResolver.php:76-82。
- API 未匹配或方法不允许:
- 现象:返回 404/405 JSON;405 时附带 allow 列表。
- 检查:BackendDeclaredMatcher 匹配结果;HTTP 方法与 declared 条目是否一致;中间件是否拦截。
- 参考路径:API Router.php:49-60、ApiResolver.php:64-70。
- 路由状态为空:
- 现象:current() 返回空 module/action/sub。
- 检查:Resolver 是否正确调用 Request::setRoute/setRouteParams;入口是否写入 routeString/routeLangSign。
- 参考路径:DelegatingRouter.php:88-108、FrontResolver.php:97-101、AdminResolver.php:93-98、ApiResolver.php:82-86。
- 新增 路由条目访问异常:
- 现象:request()->routeEntry() 返回 null。
- 检查:是否为非声明式路由;是否在正确的请求上下文中调用;RouteEntry 是否正确设置。
- 参考路径:Request.php:1318-1321、BaseController.php:84-90。
- 新增 导航元数据问题:
- 现象:后台导航高亮不正确。
- 检查:RouteEntry::$nav 字段是否正确设置;RouteNavDsl 的 nav() 方法是否正确使用;消费端是否正确处理导航元数据。
- 参考路径:RouteEntry.php:85-89、RouteNavDsl.php:48-52。
结论
DouPHP 的核心路由架构通过委托路由器模式实现了三端路由的统一抽象:DelegatingRouter 屏蔽差异、集中状态;各端 Router 仅做轻量编排;Resolver 负责声明式匹配与安全默认栈;Dispatcher 专注中间件管道与控制器执行。该设计清晰分层、易于扩展,且具备良好的错误处理与性能特性。
更新 本次更新进一步增强了路由系统的功能,通过 RouteEntry::$nav 字段和 Request::routeEntry() 方法提供了完整的后台导航高亮支持,并集成了 RouteNavDsl trait 到所有路由构建器中,实现了声明式的导航配置 API。这些改进使得开发者能够更方便地获取完整的路由上下文信息和导航元数据,同时保持了架构的简洁性和可扩展性。
附录:使用示例与扩展点
- 在入口中通过静态门面调用路由:
- 示例路径:三端 index.php 均通过 \Dou\Core\Facade\Route 调用 DelegatingRouter。
- 参考路径:DelegatingRouter.php:24-38
- 获取当前路由状态:
- 示例路径:Init::boot(Route::current()) 取 lang / is_home 等多语言初始化信息。
- 参考路径:DelegatingRouter.php:69-108
- 新增 访问匹配的路由条目:
- 示例路径:在控制器中使用
$entry = request()->routeEntry();获取完整的 RouteEntry 对象。 - 参考路径:Request.php:1318-1321、BaseController.php:84-90
- 示例路径:在控制器中使用
- 新增 使用导航元数据 DSL:
- 示例路径:在路由定义中使用
->nav(array('cur' => 'user', 'group' => 'people'))声明后台导航高亮。 - 参考路径:RouteNavDsl.php:48-52、RouteEntryBuilder.php:159
- 示例路径:在路由定义中使用
- 新增 访问导航元数据:
- 示例路径:在控制器中访问
$entry->nav['cur']获取当前导航标识。 - 参考路径:RouteEntry.php:85-89
- 示例路径:在控制器中访问
- 扩展前端路由:
- 在前台入口边界(LangPrefixParser)剥离语言前缀,并在 front/route/*.php 中添加 declared 条目。
- 参考路径:FrontResolver.php:52-106
- 扩展后台路由:
- 在 admin/route/*.php 中添加 declared 条目,并确保 HTTP 方法与规则一致。
- 参考路径:AdminResolver.php:72-104
- 扩展 API 路由:
- 在 api/route/*.php 中添加 declared 条目,并确保 HTTP 方法与规则一致。
- 参考路径:ApiResolver.php:60-91
- 自定义中间件:
- 通过 MiddlewareRegistry 的别名映射与 compose/composeFromSpec 组合默认栈与路由级细化。
- 参考路径:FrontResolver.php:174-199、AdminResolver.php:49-63、ApiResolver.php:46-51