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. 绑定:告诉容器“接口对应哪个实现”
  2. 解析:自动创建对象并注入依赖
  3. 单例:同一个类型只创建一次

三、从最简单的容器开始

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 代码。

更多推荐