为什么我们需要依赖注入容器
在面向对象编程中,类与类之间的依赖关系无处不在。一个 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 数组存储所有绑定,每个绑定是一个包含 concrete 和 shared 的数组。此外还有 $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 风格的编程提供了基础。
事件驱动。resolving 和 afterResolving 回调让开发者可以在对象解析的生命周期中插入自定义逻辑。
总结
从 60 行的手写容器到 Laravel 数千行的服务容器,核心原理并未改变:通过反射自动解析依赖、通过绑定管理抽象与实现的映射、通过单例控制对象生命周期。Laravel 的价值在于将这一原理工程化——处理了上下文绑定、循环依赖、延迟加载、扩展机制等真实场景中的复杂问题。
理解容器的最佳路径是先手写一个最小实现,再带着问题去阅读 Laravel 源码。当你明白每一行代码要解决什么问题,源码就不再晦涩,而是一份精心设计的工程蓝图。
未经允许不得转载:任鹏个人博客 » PHP 中的依赖注入容器:从手写实现到 Laravel 服务容器源码剖析


朋友圈点赞图在线生成源码