webman 中间件与依赖注入实战:洋葱模型、PHP-DI 容器与 AOP

webman 中间件与依赖注入实战:洋葱模型、PHP-DI 容器与 AOP

admin
2026-08-06 / 0 评论 / 6 阅读

webman 中间件与依赖注入实战:洋葱模型、PHP-DI 容器与 AOP

webman 的请求处理链路看似简单(路由 → 中间件 → 控制器),但中间的洋葱模型和 PHP-DI 容器藏着不少中级开发者没吃透的细节。这篇文章把中间件的执行顺序、全局/路由级/方法级三种挂载方式、依赖注入的边界一次性讲清楚,最后再聊一个常驻内存框架特有的话题——AOP。

一、洋葱模型:代码顺序与执行顺序是两回事

class AuthMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response
    {
        // ── 前置逻辑(进入时执行)──
        $userId = $this->jwt->parseToken($request);
        $request->userId = $userId;

        $response = $handler($request);  // ← 下一层洋葱

        // ── 后置逻辑(返回时执行)──
        $response->withHeader('X-Trace-Id', $request->header('x-trace-id'));
        return $response;
    }
}

三层中间件的执行顺序:

请求 →  [A前置] → [B前置] → [C前置] → Controller
响应 ←  [A后置] ← [B后置] ← [C后置] ←

最容易踩的坑:异常发生时的执行路径。 如果控制器抛异常,中间件的后置代码默认不会执行——除非你在中间件里 try/catch 包住 $handler($request)。跨域中间件经常因此"时灵时不灵":异常响应没带 CORS 头,浏览器报的不是后端错误而是跨域错误。所以 CORS 中间件要这样写:

try {
    $response = $handler($request);
} catch (\Throwable $e) {
    $response = response($e->getMessage(), 500);
}
return $response->withHeaders([
    'Access-Control-Allow-Origin'  => '*',
    'Access-Control-Allow-Headers' => 'Content-Type, Authorization',
]);

二、三种挂载粒度

1. 全局中间件(config/middleware.php)

return [
    // 每个请求都会经过
    app\middleware\AccessLog::class,
    app\middleware\Cors::class,
];

适合:日志、CORS、限流。注意全局中间件每个请求都会跑,别放重逻辑。

2. 路由/应用级中间件

// config/route.php
Route::group('/admin', function () {
    Route::resource('/users', app\admin\controller\UserController::class);
})->middleware([
    app\middleware\AdminAuth::class,
]);

// 或整个应用目录生效:app/admin/config/middleware.php
return [
    app\middleware\AdminAuth::class,
];

多应用(多入口目录)模式下,app/{应用名}/config/middleware.php 只对该应用生效——这是做多租户后台、API 版本隔离的天然工具。

3. 控制器方法级(中间件别名)

// config/middleware.php 里注册别名
return [
    'auth' => [app\middleware\Auth::class],
];

// 控制器里按方法指定
class OrderController
{
    #[Middleware(['auth'])]           // 整个控制器生效
    public function index() {}

    #[Middleware(['auth', 'throttle:60,1'])]
    public function create() {}
}

执行优先级:全局 → 应用级 → 路由级 → 方法级。同层内按注册顺序。

三、实战:三个高频中间件的正确写法

限流中间件(基于 Redis 滑动窗口)

class Throttle implements MiddlewareInterface
{
    public function __construct(private Redis $redis) {}

    public function process(Request $request, callable $handler): Response
    {
        $key = 'throttle:' . $request->getRealIp() . ':' . date('YmdHi');
        $current = $this->redis->incr($key);
        if ($current === 1) {
            $this->redis->expire($key, 60);
        }
        if ($current > 120) {
            return json(['code' => 429, 'msg' => 'too many requests'], 429);
        }
        return $handler($request);
    }
}

注意 incr 后再 expire 的写法:只在第一次设置过期,避免每请求都重置 TTL。

请求日志(含慢请求标记)

class AccessLog implements MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response
    {
        $start = microtime(true);
        $response = $handler($request);
        $cost = round((micro(true) - $start) * 1000, 2);

        Log::info('access', [
            'ip'    => $request->getRealIp(),
            'path'  => $request->path(),
            'cost'  => $cost,
            'slow'  => $cost > 500 ? '⚡' : '',
        ]);
        return $response;
    }
}

JWT 认证 + 用户上下文注入

class Auth implements MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response
    {
        $token = $request->header('authorization', '');
        try {
            $payload = JWT::decode(trim(str_replace('Bearer ', '', $token)), new Key(config('jwt.secret'), 'HS256'));
        } catch (\Throwable $e) {
            return json(['code' => 401, 'msg' => 'unauthorized'], 401);
        }
        // 挂到 request 上传递(请求级隔离,不用静态属性)
        $request->uid = $payload->uid;
        return $handler($request);
    }
}

控制器里 $request->uid 直接取——请求结束自动销毁,没有跨请求污染。

四、依赖注入:webman 的 PHP-DI 容器

webman 内置 PHP-DI。最常见的三个用法:

1. 构造器注入(推荐)

class OrderController
{
    public function __construct(
        private OrderService $orderService,   // 自动解析
        private LoggerInterface $logger,
    ) {}
}

常驻内存的特性让注入几乎零成本:控制器和依赖都在进程启动阶段实例化一次,之后每个请求直接复用。这也意味着——注入的依赖必须是"无状态"或"请求安全"的。Service 里存了请求相关的属性就会串数据。

2. 接口绑定

// config/container.php
return [
    PayInterface::class => \DI\create(WechatPay::class),
];

// 之后注入 PayInterface 拿到的就是 WechatPay

切支付渠道只改这一行。多环境(沙箱/生产)也可以在这里做条件绑定。

3. 自动装配的边界

PHP-DI 按类型提示递归解析依赖,但碰到标量参数就无能为力,需要显式定义:

// config/container.php
return [
    \DI\create(SmsClient::class)
        ->constructor(\DI\value(config('sms.appkey'))),
];

五、AOP:常驻内存框架的专属福利

AOP(面向切面编程)在 FPM 下性能代价大,但 webman 的进程常驻让织入成本只付一次。webman 通过 php-di/AOP 代理实现,最常见的场景是缓存注解

#[Cacheable(prefix: 'user:', ttl: 300)]
public function getUserProfile(int $uid): array
{
    return $this->userDao->findWithRelations($uid);
}

切面拦截方法调用:先查 Redis,命中直接返回;未命中执行原方法并写缓存。类似的还有 #[Transactional](自动事务包裹)、#[Retry](失败重试)。

两个注意点

  1. AOP 只对容器管理的对象生效——new OrderService() 出来的裸对象没有代理
  2. 拦截逻辑发生在方法级,粒度比中间件(请求级)细,适合业务横切(缓存/事务/重试),不适合请求横切(认证/日志)

六、一张图理清请求全链路

请求
 → 全局中间件(日志/CORS/限流)
   → 应用/路由中间件(认证)
     → 方法级中间件(权限)
       → 中间件层容器解析控制器(构造器注入,进程级单例)
         → 方法内 AOP 切面(缓存/事务)
           → Service(业务逻辑,保持无状态)
             → Model/Dao

每一层解决不同粒度的横切问题:请求级用中间件,业务级用 AOP,对象组装用容器——三者各司其职,别把认证写进 Service,也别把缓存注解挂在中间件上。

写在最后

中间件和依赖注入是 webman 工程化的骨架。写中间件时时刻记住两件事:异常会跳过后置逻辑、常驻内存下禁止请求级状态进静态属性;用容器时记住注入对象是进程级复用的,Service 必须无状态。下一篇讲数据库层——连接池怎么配、事务怎么包、大数据怎么跑,那是常驻内存框架最容易翻车的第二现场。

0

评论 (0)

取消
0:00