学完这篇你能得到一个能直接跑起来的 PHP 依赖注入容器:从零写出注册、自动解析、单例、接口绑定,并接上 PSR-11 标准。
第一步:先搞清容器到底解决什么
不用容器时,代码里到处是 `new`:
$log = new FileLogger('/var/log/app.log');
$db = new PdoDatabase($config, $log);
$user = new UserService($db, $log);
问题有两个:一是 `UserService` 依赖什么,调用方得全知道;二是 `$log` 这类对象每次都要重复构造。容器把「构造对象」这件事收拢到一处,调用方只说一句 `$container->get(UserService::class)`。
容器只做两件事:存工厂 和 造对象。别往里塞业务逻辑。
第二步:写出最小容器
新建 `Container.php`,先只支持手动注册:
<?php
class Container
{
private array $bindings = []; // 注册的工厂函数
private array $instances = []; // 已解析的单例
public function bind(string $id, callable $factory, bool $shared = false): void
{
$this->bindings[$id] = ['factory' => $factory, 'shared' => $shared];
}
public function singleton(string $id, callable $factory): void
{
$this->bind($id, $factory, true);
}
public function get(string $id)
{
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
if (isset($this->bindings[$id])) {
$b = $this->bindings[$id];
$object = ($b['factory'])($this);
if ($b['shared']) {
$this->instances[$id] = $object;
}
return $object;
}
return $this->build($id); // 没注册就走自动解析
}
}
工厂函数收一个 `$this`,这样注册内部还能依赖别的服务。
第三步:用反射实现自动解析
`build()` 是容器的核心:拿构造函数参数的类型提示,递归 `get()`。
private array $resolving = []; // 解析栈,用于检测循环依赖
protected function build(string $class)
{
if (!class_exists($class)) {
throw new RuntimeException("无法解析:{$class}");
}
if (isset($this->resolving[$class])) {
throw new RuntimeException('循环依赖:' . implode(' -> ', array_keys($this->resolving)));
}
$this->resolving[$class] = true;
try {
$ref = new ReflectionClass($class);
if (!$ref->isInstantiable()) {
throw new RuntimeException("{$class} 是接口或抽象类,请先 bind()");
}
$ctor = $ref->getConstructor();
if (!$ctor || $ctor->getNumberOfParameters() === 0) {
return new $class;
}
$args = [];
foreach ($ctor->getParameters() as $param) {
$type = $param->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$args[] = $this->get($type->getName());
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} elseif ($param->allowsNull()) {
$args[] = null;
} else {
throw new RuntimeException("参数 \${$param->getName()} 无法自动解析");
}
}
return $ref->newInstanceArgs($args);
} finally {
unset($this->resolving[$class]);
}
}
注意:`unset` 必须放在 `finally` 里。否则某次解析抛异常,这个类会一直卡在解析栈里,之后所有相关解析都被误判成循环依赖。
第四步:区分单例与多例
自动解析出来的对象默认按单例缓存(写进 `$instances`)。这点要留意:
注意:不是所有对象都该共享。带请求上下文的对象(当前用户、请求对象)用 `singleton()` 明确注册成共享;像 `Order` 这种数据实体千万别塞进容器,否则第二次 `get()` 拿到的是同一个实例,状态会互相污染。
第五步:绑定接口到实现
自动解析只能认类名,接口必须手动绑:
$c = new Container();
$c->singleton(LoggerInterface::class, fn($c) => new FileLogger('/var/log/app.log'));
$c->bind(UserRepositoryInterface::class, fn($c) => new MysqlUserRepository($c->get(LoggerInterface::class)));
$service = $c->get(UserService::class); // 构造参数里的接口会被自动注入
换存储只改一行注册,业务代码不动,这就是容器最实际的价值。
第六步:对齐 PSR-11
想让容器能被第三方库识别,`composer require psr/container` 后:
use Psr\Container\ContainerInterface;
class Container implements ContainerInterface
{
public function has(string $id): bool
{
return isset($this->bindings[$id]) || class_exists($id);
}
}
`get()` 签名保持 `public function get(string $id)` 即可,找不到时抛 `NotFoundExceptionInterface` 的实现。
小结
- 容器 = 存工厂 + 造对象,越薄越好,别放业务逻辑。
- 反射解析靠递归读构造函数类型提示,接口/抽象类必须先 `bind()`。
- 解析栈 + `finally` 清理,是检测循环依赖和不脏栈的关键。
- 共享实例要显式声明,请求级对象和实体对象不要缓存。
- 实现 PSR-11 接口,容器才能被框架和第三方库直接复用。