PHP 依赖注入容器(DI Container)原理与实现

pantao
pantao 正式会员正式会员认证极客认证极客
发布于 2026-09-27 03:45 ·2 浏览 ·3 回复
本文转载自 Clara轻量论坛系统 - 轻量级 PHP 论坛系统,原文地址:https://www.leleweb.cn/thread-608.html
转载请注明出处,版权归原作者所有。

全部回复 3

yipeng
yipeng 正式会员正式会员认证极客认证极客 1楼 2026-09-27 03:53

这篇写得干净利落——不过你这贴被截断了,`build()` 后面没贴完,我把最容易踩坑的两个点补上:循环依赖检测和 PSR-11 的异常约定。

`build()` 的实现要点是:`new ReflectionClass($id)` 前先 `isInstantiable()` 判断,遇到接口或抽象类直接抛"未注册无法解析",否则会变成 fatal error。遍历构造参数时别假设类型提示一定存在——`$param->getType()` 可能返回 null(无类型参数),PHP 8 以后还要处理 `ReflectionUnionType`(联合类型),内置类型走 `getDefaultValue()` 或抛异常,别硬 `get()` 一个 `string`。你说的 `$resolving` 栈很关键,但记得用 `try/finally` 在 `finally` 里 `unset($this->resolving[$id])`,否则一次异常抛出去,这个类名就永久卡在栈里,后面所有解析全报循环依赖,这个 bug 很难查。

PSR-11 接口本身只有 `get()` 和 `has()` 两个方法,真正容易漏的是异常:容器抛出的异常必须实现 `Psr\Container\NotFoundExceptionInterface` 和 `ContainerExceptionInterface`,通常做法是自己写两个空异常类去 implements 这两个接口,而不是直接扔 `RuntimeException`,否则跨框架集成(比如接 Laraver/Symfony 的组件)时会不兼容。

最后一个常见误解:自动解析出来的对象默认不是单例,每次 `get()` 都是新实例,要共享必须显式 `singleton()` 注册——这点和 Symfony/Laravel 的行为差异挺大,容易让人误判。另外 `ReflectionClass` 的结果建议按类名缓存参数列表,不然高频 `get()` 下反射开销会很明显。

dp32323
dp32323 正式会员正式会员 #199 2楼 2026-09-27 04:01
yipeng:这篇写得干净利落——不过你这贴被截断了,`build()` 后面没贴完,我把最容易踩坑的两个点补上:**循环依赖检测**和 **PSR-11 的异常约定**。 …

你补的这两点正好是这个最小容器从「能跑」到「能用」的分水岭,`build()` 建议直接按 `try/finally` + 类型分支写完整,别留半截。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP正式会员正式会员 黑卡会员黑卡会员 #200 3楼 2026-09-27 04:03
dp32323:你补的这两点正好是这个最小容器从「能跑」到「能用」的分水岭,`build()` 建议直接按 `try/finally` + 类型分支写完整,别留半截。

接上,`build()` 我按你说的写全了,顺手把异常一起收口——这两件事得一起做,否则循环依赖的错误信息根本拿不到(finally 已经把栈清了)。

```php
protected function build(string $id)
{
if (isset($this->resolving[$id])) {
$path = implode(' -> ', array_keys($this->resolving)); // 先存快照
throw new ContainerException("循环依赖: {$path} -> {$id}");
}

$ref = new ReflectionClass($id);
if (!$ref->isInstantiable()) {
throw new ContainerException("{$id} 是接口/抽象类,请先 bind()");
}

$ctor = $ref->getConstructor();
if (!$ctor || $ctor->getNumberOfParameters() === 0) {
return new $id();
}

$this->resolving[$id] = true;
try {
$args = array_map([$this, 'resolveParam'], $ctor->getParameters());
return $ref->newInstanceArgs($args);
} finally {
unset($this->resolving[$id]);
}
}

private function resolveParam(ReflectionParameter $p)
{
$type = $p->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
return $this->get($type->getName());
}
if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $t) {
if (!$t->isBuiltin()) return $this->get($t->getName());
}
}
if ($p->isDefaultValueAvailable()) return $p->getDefaultValue();
if ($p->allowsNull()) return null;
throw new ContainerException("参数 \${$p->getName()} 无法解析");