依赖注入
概念说明
依赖注入(Dependency Injection,简称 DI)是一种设计模式,通过外部将依赖传递给对象,而非在对象内部自行创建。Viswoole 框架的 Container(容器) 是依赖注入的核心实现,它负责:
- 服务绑定(bind) — 将接口或类名映射到具体的实现类或闭包
- 实例化(make) — 自动解析构造函数依赖,递归创建所需对象
- 获取实例(get) — 从容器中获取已绑定的服务实例
- 方法调用(invoke) — 自动注入方法参数并执行
工作原理
容器的角色
App 继承自 Container,是整个框架的服务容器。当需要某个类的实例时,容器会:
- 检查该类是否已绑定到具体实现
- 通过反射分析构造函数的参数类型
- 递归解析每个参数的依赖
- 创建并返回完整的实例
协程上下文隔离
在 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}");
}
}