PHP 中的依赖注入(DI)容器原理剖析:手写一个迷你容器
·
PHP 中的依赖注入(DI)容器原理剖析:手写一个迷你容器
如果你用过 Laravel、Symfony、ThinkPHP 6+,那么你一定见过这样的代码:
app(UserService::class)->info();
或者:
public function __construct(UserRepository $repo)
这些“魔法”的背后,核心只有两个字:容器(Container)。
但容器到底是什么?它是如何自动“变出”对象的?为什么构造函数里写个类型,就能自动注入?
今天,我们不背概念,从零手写一个迷你 DI 容器,彻底搞懂它的底层原理。
一、为什么需要依赖注入容器?
1. 没有容器的世界
$config = new Config();
$db = new Database($config);
$repo = new UserRepository($db);
$service = new UserService($repo);
问题显而易见:
- 对象创建耦合严重
- 依赖关系混乱
- 修改一个类,牵连一串
2. 依赖注入(DI)解决了什么?
依赖注入的核心思想:
对象的依赖,不由自己创建,而是由外部“注入”。
class UserService
{
public function __construct(
private UserRepository $repo
) {}
}
但问题来了:
谁来负责“注入”?
👉 容器
二、DI 容器的本质是什么?
一句话总结:
DI 容器 = 对象工厂 + 依赖自动解析 + 实例管理
它主要干三件事:
- 绑定:告诉容器“接口对应哪个实现”
- 解析:自动创建对象并注入依赖
- 单例:同一个类型只创建一次
三、从最简单的容器开始
1. 最基础的“对象工厂”
class Container
{
protected array $bindings = [];
public function bind(string $abstract, callable $factory)
{
$this->bindings[$abstract] = $factory;
}
public function make(string $abstract)
{
return $this->bindings[$abstract]($this);
}
}
使用方式:
$container = new Container();
$container->bind(UserService::class, function ($c) {
return new UserService(new UserRepository());
});
$service = $container->make(UserService::class);
✅ 解耦了对象创建
❌ 依赖还是要手写
四、自动解析构造函数(核心难点)
1. 利用 PHP 反射(Reflection)
PHP 提供了 ReflectionClass,可以“看清”一个类的内部结构。
$reflector = new ReflectionClass(UserService::class);
$constructor = $reflector->getConstructor();
如果构造函数存在,我们可以拿到它的参数:
$params = $constructor->getParameters();
每个参数可以拿到:
- 参数名
- 类型(Type)
- 是否必填
- 默认值
2. 递归解析依赖
这是容器的灵魂逻辑。
protected function resolve(string $abstract)
{
$reflector = new ReflectionClass($abstract);
if (!$reflector->isInstantiable()) {
throw new Exception("不能实例化 {$abstract}");
}
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $abstract();
}
$dependencies = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if (!$type || $type->isBuiltin()) {
throw new Exception("无法解析参数 {$param->getName()}");
}
$dependencies[] = $this->make($type->getName());
}
return $reflector->newInstanceArgs($dependencies);
}
✅ 自动解析依赖
✅ 支持嵌套依赖
✅ 真正的“自动注入”
五、支持绑定接口与实现
1. 接口绑定实现
$container->bind(UserRepositoryInterface::class, function () {
return new MySQLUserRepository();
});
2. 修改 make 方法
public function make(string $abstract)
{
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]($this);
}
return $this->resolve($abstract);
}
这样,当容器遇到:
public function __construct(UserRepositoryInterface $repo)
会自动返回 MySQLUserRepository。
六、单例(Singleton)支持
1. 为什么要单例?
- 数据库连接
- 配置对象
- 日志对象
这些对象全局只应有一个实例。
2. 实现单例
class Container
{
protected array $bindings = [];
protected array $instances = [];
public function singleton(string $abstract, callable $factory)
{
$this->bind($abstract, $factory);
$this->instances[$abstract] = null;
}
public function make(string $abstract)
{
if (array_key_exists($abstract, $this->instances)) {
if ($this->instances[$abstract] === null) {
$this->instances[$abstract] = $this->bindings[$abstract]($this);
}
return $this->instances[$abstract];
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]($this);
}
return $this->resolve($abstract);
}
}
✅ 同一个类,永远返回同一个对象
七、完整迷你容器代码
class Container
{
protected array $bindings = [];
protected array $instances = [];
public function bind(string $abstract, callable $factory)
{
$this->bindings[$abstract] = $factory;
}
public function singleton(string $abstract, callable $factory)
{
$this->bindings[$abstract] = $factory;
$this->instances[$abstract] = null;
}
public function make(string $abstract)
{
if (array_key_exists($abstract, $this->instances)) {
if ($this->instances[$abstract] === null) {
$this->instances[$abstract] = $this->bindings[$abstract]($this);
}
return $this->instances[$abstract];
}
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]($this);
}
return $this->resolve($abstract);
}
protected function resolve(string $abstract)
{
$reflector = new ReflectionClass($abstract);
if (!$reflector->isInstantiable()) {
throw new Exception("无法实例化 {$abstract}");
}
$constructor = $reflector->getConstructor();
if (is_null($constructor)) {
return new $abstract();
}
$dependencies = [];
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if (!$type || $type->isBuiltin()) {
throw new Exception("无法解析参数 {$param->getName()}");
}
$dependencies[] = $this->make($type->getName());
}
return $reflector->newInstanceArgs($dependencies);
}
}
八、和 Laravel 容器的关系
你写的这个容器,其实就是 Laravel Container 的极简版。
Laravel 容器额外支持:
- 上下文绑定
- 别名
- 自动注入方法参数
- 闭包绑定
- Rebinding
- 扩展器(extend)
但核心原理完全一致。
九、常见误区澄清
❌ 容器 = 服务定位器?
不是。
- ✅ 容器:依赖由外部注入(DI)
- ❌ 服务定位器:对象主动找容器(反模式)
❌ 容器一定要用反射?
不一定。
- 反射灵活但慢
- 生产环境可“编译”容器(如 Symfony)
十、总结
依赖注入容器的本质,就是一个“聪明的对象工厂”。
它通过:
- ✅ 反射解析依赖
- ✅ 自动递归创建对象
- ✅ 管理单例生命周期
让你可以写出:
- 低耦合
- 可测试
- 可维护
的现代 PHP 代码。
更多推荐
所有评论(0)