文档目录
自定义中间件开发

简介

本技术文档面向 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 逐步缩小范围。
  • 参数化开关:利用参数化中间件在开发环境开启详细日志,生产环境关闭。
  • 观察响应头:确认安全头与缓存头是否正确下发。

最佳实践

  • 单一职责:每个中间件只做一件事。
  • 无副作用:避免在中间件中修改共享状态。
  • 防御式编程:对输入做校验,对异常做兜底。
  • 可配置:通过配置与参数化控制行为,避免硬编码。
  • 可观测:埋点日志与指标上报,便于追踪与告警。
添加日期:2026-10-05