依赖注入

概念说明

依赖注入(Dependency Injection,简称 DI)是一种设计模式,通过外部将依赖传递给对象,而非在对象内部自行创建。Viswoole 框架的 Container(容器) 是依赖注入的核心实现,它负责:

  • 服务绑定(bind) — 将接口或类名映射到具体的实现类或闭包
  • 实例化(make) — 自动解析构造函数依赖,递归创建所需对象
  • 获取实例(get) — 从容器中获取已绑定的服务实例
  • 方法调用(invoke) — 自动注入方法参数并执行

工作原理

容器的角色

App 继承自 Container,是整个框架的服务容器。当需要某个类的实例时,容器会:

  1. 检查该类是否已绑定到具体实现
  2. 通过反射分析构造函数的参数类型
  3. 递归解析每个参数的依赖
  4. 创建并返回完整的实例

协程上下文隔离

在 Swoole 协程环境中,容器使用 Context 按顶级协程 ID 隔离单例。这意味着每个协程拥有独立的服务实例,避免并发状态污染。

参数注入支持

容器的参数注入能力覆盖以下场景:

注入方式说明
命名参数通过参数名匹配注入值
位置参数按参数位置顺序注入
可变参数支持 ...$args 可变参数
默认值参数有默认值时可省略
可空类型支持可空类型参数 ?Type
扩展规则通过 Attribute 定义额外注入规则
前置注入实现 PreInjectInterface 在注入前自定义处理

代码示例

构造函数注入

最常用的注入方式,通过类型提示让容器自动解析依赖:

php
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class UserController
{
    public function __construct(
        private RequestInterface $request,
        private ResponseInterface $response,
    ) {}

    public function show(int $id): ResponseInterface
    {
        // 直接使用注入的依赖
        return $this->response->withStatus(200);
    }
}

// 容器自动解析并注入 Request 和 Response
$userController = app()->make(UserController::class);

接口绑定与实现

将接口绑定到具体实现类,实现依赖倒置:

php
// 绑定接口到实现类
app()->bind(CacheInterface::class, RedisCache::class);

// 或使用闭包进行更复杂的绑定
app()->bind(DatabaseInterface::class, function (Container $container) {
    $config = config('database');
    return new MySqlConnection($config);
});

// 解析时自动使用绑定的实现
$cache = app()->make(CacheInterface::class); // 返回 RedisCache 实例

单例绑定

确保一个类在当前协程上下文内只创建一次(协程环境下按顶级协程 ID 隔离):

php
// 单例绑定:多次 make 返回同一实例
app()->bind(Logger::class, FileLogger::class);

$logger1 = app()->make(Logger::class);
$logger2 = app()->make(Logger::class);

assert($logger1 === $logger2); // true

方法调用与参数注入

容器可以自动注入方法的参数:

php
class OrderService
{
    /**
     * 处理订单,容器会自动注入 UserRepository 和 PaymentService
     */
    public function createOrder(
        UserRepository $repo,
        PaymentService $payment,
        int $userId,
        string $orderNo,
    ): Order {
        $user = $repo->find($userId);
        return $payment->process($user, $orderNo);
    }
}

// invoke 自动解析方法参数并执行
$result = app()->invoke([new OrderService(), 'createOrder'], [
    'userId' => 1,
    'orderNo' => 'ORD20240101',
]);

全局辅助函数

框架提供便捷的全局函数来操作容器:

php
// 获取容器实例
$app = app();

// 获取配置值
$timezone = config('app.default_timezone'); // 'Asia/Shanghai'

// 获取环境变量
$debug = env('APP_DEBUG', false);

// 快速解析类
$cache = make(RedisCache::class);

// 手动绑定
bind(Logger::class, FileLogger::class);

最佳实践

1. 优先使用接口绑定

始终面向接口编程,便于替换实现和单元测试:

php
// ✅ 推荐:绑定接口
app()->bind(MailServiceInterface::class, SmtpMailService::class);

// ❌ 不推荐:直接绑定具体类(除非确实不需要抽象)
app()->bind(SmtpMailService::class, SmtpMailService::class);

2. 保持构造函数简洁

构造函数只注入必要的依赖,避免过多参数:

php
// ✅ 推荐:职责清晰,参数可控
class UserService
{
    public function __construct(
        private UserRepository $repository,
        private EventDispatcher $events,
    ) {}
}

// ❌ 不推荐:构造函数参数过多,说明类可能承担了过多职责
class UserService
{
    public function __construct(
        private RepoA $a,
        private RepoB $b,
        private ServiceC $c,
        private ServiceD $d,
        private ServiceE $e,
    ) {}
}

3. 利用协程隔离特性

在协程环境中,有状态的服务应使用单例绑定,容器会自动按协程隔离:

php
// 数据库连接等有状态服务,每个协程持有独立实例
app()->bind(ConnectionPool::class, function () {
    return new ConnectionPool(config('database'));
});

4. 避免服务定位器反模式

不要在业务代码中过度使用 app()->make() 进行服务定位:

php
// ❌ 不推荐:在业务逻辑中使用服务定位
class UserController
{
    public function show(int $id)
    {
        $logger = app()->make(Logger::class); // 隐藏依赖关系
        $logger->info("查看用户 {$id}");
    }
}

// ✅ 推荐:通过构造函数声明依赖
class UserController
{
    public function __construct(private Logger $logger) {}

    public function show(int $id)
    {
        $this->logger->info("查看用户 {$id}");
    }
}