PHP 中的依赖注入容器:从手写实现到 Laravel 服务容器源码剖析

为什么我们需要依赖注入容器

在面向对象编程中,类与类之间的依赖关系无处不在。一个 UserController 需要 UserRepository,而 UserRepository 又需要 DatabaseConnection。如果每个类都在内部自行实例化依赖,代码将变得高度耦合、难以测试、难以维护。

依赖注入(Dependency Injection)将依赖的创建权从类内部转移到外部,而依赖注入容器(DI Container)则是将这一过程自动化的工具。它负责注册、解析和管理对象的生命周期,让开发者从繁琐的 new 操作中解放出来。

手写一个简易容器

理解容器最好的方式是从零实现一个。核心思路只有两点:注册绑定自动解析

class Container
{
    protected array $bindings = [];
    protected array $instances = [];

    // 注册绑定
    public function bind(string $abstract, Closure $concrete, bool $shared = false): void
    {
        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    // 注册单例
    public function singleton(string $abstract, Closure $concrete): void
    {
        $this->bind($abstract, $concrete, true);
    }

    // 解析
    public function make(string $abstract)
    {
        // 已解析的单例直接返回
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $concrete = $this->bindings[$abstract]['concrete'] ?? null;

        if ($concrete === null) {
            // 没有绑定,尝试反射自动解析
            $object = $this->build($abstract);
        } else {
            $object = $concrete($this);
        }

        // 如果是单例,缓存实例
        if (($this->bindings[$abstract]['shared'] ?? false)) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }

    // 通过反射自动构建
    protected function build(string $class)
    {
        $reflector = new ReflectionClass($class);

        if (!$reflector->isInstantiable()) {
            throw new Exception("无法实例化 {$class}");
        }

        $constructor = $reflector->getConstructor();
        if ($constructor === null) {
            return new $class;
        }

        $dependencies = [];
        foreach ($constructor->getParameters() as $param) {
            $type = $param->getType();
            if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
                $dependencies[] = $this->make($type->getName());
            } elseif ($param->isDefaultValueAvailable()) {
                $dependencies[] = $param->getDefaultValue();
            } else {
                throw new Exception("无法解析参数 {$param->getName()}");
            }
        }

        return $reflector->newInstanceArgs($dependencies);
    }
}

这个约 60 行的容器已经具备了 DI 容器的核心能力:绑定接口到实现、单例管理、通过反射自动解析依赖树。当我们调用 $container->make(UserController::class) 时,容器会自动递归解析 UserController 构造函数中的所有类型提示依赖,直至构建出完整的对象图。

Laravel 服务容器的架构剖析

Laravel 的服务容器(Illuminate\Container\Container)建立在上述原理之上,但在工程化方面做了大量增强。其核心结构可以从以下几个维度理解。

绑定管理:bindings 与 aliases

Laravel 用 $bindings 数组存储所有绑定,每个绑定是一个包含 concreteshared 的数组。此外还有 $aliases 用于处理别名映射,$abstractAliases 反向索引别名。当你执行 $app->bind(Logger::class, FileLogger::class) 时,容器记录的是抽象到具体实现的映射关系。

$instances 数组则存储已解析的单例对象。$resolved 数组记录哪些抽象已被解析过,用于触发 resolving 回调。

解析流程:resolve 方法的精妙设计

Laravel 的 resolve 方法比手写版本复杂得多,它处理了多种边界情况:

public function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    // 如果已有实例且无额外参数,直接返回
    if (isset($this->instances[$abstract]) && !$parameters) {
        return $this->instances[$abstract];
    }

    $concrete = $this->getConcrete($abstract);

    // 如果 concrete 是闭包或等于 abstract,直接 build
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        // 否则递归解析 concrete
        $object = $this->make($concrete);
    }

    // 触发扩展(extenders)
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // 如果是单例,存入 instances
    if ($this->isShared($abstract) && !$parameters) {
        $this->instances[$abstract] = $object;
    }

    // 触发 resolving 回调
    $this->fireResolvingCallbacks($abstract, $object);

    return $object;
}

关键差异在于:Laravel 支持 extender(在对象构建后对其进行装饰)、resolving 回调(在解析完成时触发事件),以及 上下文绑定(同一个接口在不同类中注入不同实现)。

build 方法:反射与上下文依赖

build 方法是容器真正“干活”的地方。它通过 ReflectionClass 获取构造函数参数,然后逐个解析:

protected function build($concrete)
{
    if ($concrete instanceof Closure) {
        return $concrete($this, $this->getLastParameterOverride());
    }

    $reflector = new ReflectionClass($concrete);

    if (!$reflector->isInstantiable()) {
        return $this->notInstantiable($concrete);
    }

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
        return new $concrete;
    }

    $dependencies = $constructor->getParameters();

    try {
        $instances = $this->resolveDependencies($dependencies);
    } catch (BindingResolutionException $e) {
        array_pop($this->buildStack);
        throw $e;
    }

    return $reflector->newInstanceArgs($instances);
}

resolveDependencies 遍历每个参数,处理类型提示、默认值、可变参数等情况。对于类类型提示,它调用 resolveClass,其中会检查上下文绑定($this->contextual),如果当前构建栈中存在针对该类的上下文绑定,则使用特定实现。

上下文绑定:解决同一接口不同实现的问题

假设 ReportController 需要 MySQLExporter,而 CacheController 需要 RedisExporter,两者都依赖 Exporter 接口。Laravel 的解决方案是:

$this->app->when(ReportController::class)
    ->needs(Exporter::class)
    ->give(MySQLExporter::class);

$this->app->when(CacheController::class)
    ->needs(Exporter::class)
    ->give(RedisExporter::class);

容器在 resolveClass 时,会通过 getContextualConcrete 检查构建栈的顶端类是否有对应的上下文绑定,从而注入不同的实现。

从源码中汲取的设计智慧

Laravel 服务容器之所以强大,在于它解决了一系列工程化问题:

递归解析与循环依赖检测buildStack 数组记录当前构建路径,当检测到循环依赖时抛出异常,避免无限递归。

延迟加载与性能优化$deferredServices 允许服务提供者延迟注册,只有在真正需要时才加载,减少每次请求的初始化开销。

扩展机制extend 方法允许在对象解析后对其进行包装或修改,这为 AOP 风格的编程提供了基础。

事件驱动resolvingafterResolving 回调让开发者可以在对象解析的生命周期中插入自定义逻辑。

总结

从 60 行的手写容器到 Laravel 数千行的服务容器,核心原理并未改变:通过反射自动解析依赖、通过绑定管理抽象与实现的映射、通过单例控制对象生命周期。Laravel 的价值在于将这一原理工程化——处理了上下文绑定、循环依赖、延迟加载、扩展机制等真实场景中的复杂问题。

理解容器的最佳路径是先手写一个最小实现,再带着问题去阅读 Laravel 源码。当你明白每一行代码要解决什么问题,源码就不再晦涩,而是一份精心设计的工程蓝图。

未经允许不得转载:任鹏个人博客 » PHP 中的依赖注入容器:从手写实现到 Laravel 服务容器源码剖析

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏