简介
本技术文档面向 DouPHP 的自定义中间件开发,系统讲解如何基于框架提供的接口与抽象类实现可复用、可参数化、可配置的中间件。内容覆盖:
- 中间件接口与管道执行模型
- 参数化中间件的参数注入机制与配置管理
- 全局注册、路由级注册与条件注册方式
- 常见场景示例:日志记录、数据转换、缓存控制、鉴权限流等
- 性能优化、调试技巧与最佳实践
项目结构
DouPHP 将中间件能力集中在 core/foundation/middleware 中,并通过各端(front/admin/api)的 Resolver 与 init/middleware.php 完成别名映射与默认栈装配;路由声明式 DSL 支持在 RouteEntry 上附加中间件细化字段,最终由 MiddlewareRegistry 组装为实例链,交由 Pipeline 顺序执行。
graph TB
subgraph "核心中间件"
MI["MiddlewareInterface"]
PMW["ParameterizedMiddleware"]
REG["MiddlewareRegistry"]
PIPE["MiddlewarePipeline"]
AUA["AbstractUserAuthMiddleware"]
ASH["AbstractSecurityHeadersMiddleware"]
end
subgraph "端侧实现"
API_RES["ApiResolver"]
AUTH_API["Api\\Middleware\\UserAuthMiddleware"]
SEC_FRONT["Front\\Middleware\\SecurityHeadersMiddleware"]
AUTH_ADMIN["Admin\\Middleware\\AuthMiddleware"]
end
subgraph "路由与分发"
RE["RouteEntry"]
RRB["RouteResourceBuilder"]
REB["RouteEntryBuilder"]
DISP["Dispatcher"]
end
MI --> PIPE
PMW --> REG
AUA --> AUTH_API
ASH --> SEC_FRONT
API_RES --> REG
REG --> PIPE
RE --> REG
RRB --> RE
REB --> RE
PIPE --> DISP
核心组件
- 中间件接口:定义 handle($next) 契约,要求放行时 return $next(),禁止吞掉下游返回值。
- 参数化中间件接口:提供 setRouteParameters(array $params),用于接收路由级参数(字符串位置数组)。
- 中间件注册表:负责别名到类的映射、默认栈过滤豁免、追加路由级中间件、解析别名 DSL 并生成实例链。
- 中间件管道:按顺序执行中间件链,最终调用控制器动作。
- 鉴权基类:封装公共鉴权骨架(public/optional/required + work 子策略),子类仅实现端差异。
- 安全头基类:统一下发安全响应头,三端薄壳继承即可。
架构总览
请求进入后,Resolver 根据 URL 匹配路由条目(RouteEntry),读取其携带的中间件细化字段(skip_all_middleware、without_middleware、middleware、middleware_params),结合该端的全局默认别名栈,通过 MiddlewareRegistry 组装出最终的中间件实例列表,再交给 Pipeline 依次执行,最后调用控制器方法。
sequenceDiagram
participant Client as "客户端"
participant Resolver as "端侧Resolver"
participant Reg as "MiddlewareRegistry"
participant Pipe as "MiddlewarePipeline"
participant MW as "中间件链"
participant Ctrl as "控制器"
Client->>Resolver : "HTTP 请求"
Resolver->>Resolver : "匹配路由 -> RouteEntry"
Resolver->>Reg : "compose(默认别名栈, RouteEntry)"
Reg-->>Resolver : "中间件实例数组"
Resolver->>Pipe : "make([...middlewares])"
Pipe->>MW : "按序执行 handle(next)"
MW-->>Ctrl : "命中控制器动作"
Ctrl-->>Client : "响应"
详细组件分析
中间件接口与管道
- 接口契约:handle($next) 必须 return $next(),否则下游响应会被丢弃。
- 管道执行:使用 array_reduce 反向构建闭包链,保证外层先入先出执行顺序。
flowchart TD
Start(["进入管道"]) --> M1["中间件A.handle(next)"]
M1 --> |return next()| M2["中间件B.handle(next)"]
M2 --> |return next()| Ctrl["控制器动作"]
Ctrl --> |返回响应| M2
M2 --> |冒泡| M1
M1 --> |冒泡| End(["返回响应"])
参数化中间件与配置管理
- 参数注入:当路由声明了带参数的别名(如 throttle:5,60),MiddlewareRegistry 会为对应别名新建独占实例并调用 setRouteParameters(['5','60'])。
- 优先级:fluent 糖 middlewareParams 覆盖 > 别名内联 DSL 参数。
- 配置加载:以 AbstractUserAuthMiddleware 为例,构造时从端侧配置文件(如 api/init/middleware.php)加载 auth_modes 与 work_required,并在 handle 中决策 public/optional/required 与是否校验工作身份。
classDiagram
class ParameterizedMiddleware {
+setRouteParameters(params) void
}
class AbstractUserAuthMiddleware {
-authModes : array
-workRequired : array
-routeModeOverride : string?
+__construct()
+handle(next) mixed
#configFile() string
#resolveContext() array
#inject(context) void
#hasWorkIdentity() bool
#rejectUnauthenticated() void
#rejectForbidden() void
}
class UserAuthMiddleware {
+configFile() string
+resolveContext() array
+inject(context) void
+hasWorkIdentity() bool
+rejectUnauthenticated() void
+rejectForbidden() void
}
ParameterizedMiddleware <|.. AbstractUserAuthMiddleware
AbstractUserAuthMiddleware <|-- UserAuthMiddleware
安全响应头中间件
- 行为:在管道最前置下发一组基线安全头(X-Content-Type-Options / X-Frame-Options / Referrer-Policy / Permissions-Policy),HTTPS 且开启时下发 HSTS。
- 三端复用:前端薄壳 SecurityHeadersMiddleware 直接继承基类,无需重复实现。
后台认证中间件
- 职责:恢复管理员登录态,未登录抛出异常跳转登录页;已登录则放行。
- 免登入口:通过路由级 withoutMiddleware 声明式豁免,不在中间件内硬编码白名单。
路由级中间件 DSL 与装配
- 路由条目字段:skip_all_middleware、without_middleware、middleware、middleware_params。
- 装配逻辑:
- skipAllMiddleware 返回空链(最高优先级)
- without 从默认栈过滤指定别名
- middleware 末尾追加额外别名(支持 alias:p1,p2)
- middleware_params 给对应别名新建带参实例(不污染共享默认)
- 别名 DSL:alias:p1,p2,参数全为字符串,逗号分隔,禁嵌套。
flowchart TD
S["开始装配"] --> Skip{"skip_all_middleware?"}
Skip --> |是| Empty["返回空链"]
Skip --> |否| Filter["过滤 without 别名"]
Filter --> Append["追加路由级 middleware"]
Append --> Parse["解析别名DSL与参数"]
Parse --> Make["容器实例化 + 参数注入"]
Make --> Return["返回实例链"]
依赖关系分析
- 端侧 Resolver 维护别名到 FQCN 的映射,缺类时跳过(避免模块卸载导致致命错误)。
- Dispatcher 在中间件执行前将路由参数写入 Request,确保中间件与控制器均可访问路径段参数。
- 路由构建器(resource/group)通过 fluent DSL 将中间件字段合并进 RouteEntry,最终参与装配。
graph LR
API_RES["ApiResolver"] --> ALIAS["别名映射"]
ALIAS --> REG["MiddlewareRegistry"]
REG --> PIPE["MiddlewarePipeline"]
RE["RouteEntry"] --> REG
RRB["RouteResourceBuilder"] --> RE
REB["RouteEntryBuilder"] --> RE
PIPE --> DISP["Dispatcher"]
性能与优化
- 最小化中间件数量:仅在必要时启用,优先使用 skip_all_middleware 或 without 剔除无关中间件。
- 避免阻塞 I/O:不要在中间件中进行耗时同步操作(如远程调用、大文件 IO),必要时异步化或降级。
- 合理使用缓存:对只读数据(如配置、字典)进行短生命周期缓存,减少重复查询。
- 参数化中间件按需实例化:路由级参数会创建独占实例,避免在共享默认实例上修改状态。
- 响应头尽早设置:安全头在管道最前置下发,减少后续处理开销。
故障排查指南
- 中间件被静默跳过:检查别名是否注册、类是否存在;Resolver 会在缺类时跳过,不会 fatal。
- 响应被吞掉:确认中间件放行时 return $next(),不要丢弃下游返回值。
- 参数未生效:确认路由声明了 middleware_params 或别名 DSL,且中间件实现了 ParameterizedMiddleware。
- 鉴权失败:核对端侧 middleware.php 中的 auth_modes 与 work_required 配置是否正确覆盖目标模块/动作。
- 安全头未下发:确认 config/security.headers 已配置且 headers_sent() 为 false。
结论
DouPHP 的中间件体系通过清晰的接口契约、灵活的别名 DSL 与分层装配机制,使开发者可以以最小成本实现高内聚、低耦合的可插拔横切逻辑。借助参数化中间件与端侧配置,既能满足通用需求,又能精细控制不同路由的行为。遵循本文的最佳实践,可在保证安全与性能的前提下快速扩展系统能力。
附录:完整开发流程与示例
开发流程
- 明确职责:确定中间件要处理的横切关注点(鉴权、限流、日志、缓存、安全头等)。
- 选择基类:
- 鉴权类:继承 AbstractUserAuthMiddleware,实现端侧差异方法。
- 安全头类:继承 AbstractSecurityHeadersMiddleware。
- 其他:实现 MiddlewareInterface。
- 参数化:如需路由级参数,实现 ParameterizedMiddleware 的 setRouteParameters。
- 配置:在端侧 middleware.php 中登记 auth_modes/work_required(鉴权类)。
- 注册:
- 全局注册:在端侧 Resolver 的别名映射中添加 alias => FQCN。
- 路由注册:在路由声明中使用 middleware('alias:p1,p2') 或 middlewareParams。
- 条件注册:通过 skip_all_middleware 或 without 控制是否启用。
- 测试验证:覆盖正常放行、拒绝、参数注入、配置覆盖等用例。
示例一:日志记录中间件
- 目标:记录请求 URI、方法、耗时、状态码。
- 实现要点:
- 实现 MiddlewareInterface::handle,记录开始时间,调用 $next(),记录结束时间与响应信息。
- 避免阻塞:I/O 尽量异步或落盘批处理。
- 可选参数化:实现 ParameterizedMiddleware,支持按模块/级别过滤。
- 注册:
- 全局:在端侧 Resolver 别名映射中加入日志中间件。
- 路由:仅对特定路由启用,减少噪音。
示例二:数据转换中间件
- 目标:统一输入格式(如 JSON 转数组)、输出格式化(如统一包装响应体)。
- 实现要点:
- 在 handle 中读取/改写 Request 数据或 Response。
- 注意幂等性与异常保护,避免破坏下游业务。
- 注册:
- 路由级:仅对需要转换的接口启用。
- 参数化:支持按模块/动作开关。
示例三:缓存控制中间件
- 目标:对 GET 请求添加 ETag/Last-Modified 或短期缓存头。
- 实现要点:
- 依据路由键生成缓存键,命中则直接返回 304。
- 写缓存需考虑并发与失效策略。
- 注册:
- 路由级:对静态资源或热点接口启用。
- 参数化:支持 TTL、缓存后端等。
示例四:鉴权与限流组合
- 鉴权:参考 Api\UserAuthMiddleware 与 AbstractUserAuthMiddleware,按端侧 middleware.php 配置决定 public/optional/required 与工作身份校验。
- 限流:实现限流中间件(可参考 Throttle 思路),支持速率限制与窗口控制,通过别名 DSL 传入阈值与周期。
- 组合:在路由上同时挂载鉴权与限流,顺序建议“鉴权在前、限流在后”,以减少无效请求计数。
调试技巧
- 打印中间件链:在 Dispatcher 前后记录中间件名称与执行顺序,定位问题链路。
- 逐步禁用:通过 without 或 skip_all_middleware 逐步缩小范围。
- 参数化开关:利用参数化中间件在开发环境开启详细日志,生产环境关闭。
- 观察响应头:确认安全头与缓存头是否正确下发。
最佳实践
- 单一职责:每个中间件只做一件事。
- 无副作用:避免在中间件中修改共享状态。
- 防御式编程:对输入做校验,对异常做兜底。
- 可配置:通过配置与参数化控制行为,避免硬编码。
- 可观测:埋点日志与指标上报,便于追踪与告警。