依赖注入容器的本质是一台对象装配机。基于 PHP 反射 API 读取构造函数签名,即可完成参数的自动解析与递归注入;再配合由实例缓存、定义缓存、反射元数据缓存组成的三级缓存池,可将反射开销压缩到可忽略区间。以下内容基于 PHP 8.1 与 PSR-11 标准,给出一个不足 150 行、可直接用于生产环境的容器实现,覆盖服务注册、单例绑定、闭包工厂、循环依赖检测等完整能力。
依赖注入容器是现代 PHP 框架的基础设施。Laravel 的 Illuminate\Container、Symfony 的 DependencyInjection 组件、PHP-DI 都围绕同一核心运转:集中管理对象的创建方式,调用方只声明所需类型。理解并手写一个容器,是掌握自动装配原理的最短路径。
一、为什么需要依赖注入容器
1、控制反转与依赖注入的关系
依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IoC)的一种落地形式:类不再自行 new 出依赖,而是在构造函数中声明所需类型,由外部装配者提供实例。当依赖链变长——例如 UserService 依赖 Logger 与 PDO,Logger 又依赖配置对象——手工逐层实例化会产生大量重复样板代码,且替换实现时必须改动每一处调用点。
2、容器承担的职责
依赖注入容器把“如何创建对象”这一知识集中管理:服务如何注册、如何解析构造参数、是否共享单例,全部收敛到容器内部。业务代码只面对 $container->get(Service::class) 一个入口,依赖图由容器自动展开。对象之间的耦合从编译期转移到了配置期,单元测试中替换 mock 实现也只需改动一处绑定。
二、容器的核心结构设计
1、服务定义表
定义表是服务标识与创建方式的映射。创建方式通常支持三种形态:类名字符串(交给反射工厂自动装配)、闭包(自定义工厂函数)、已创建实例(直接托管给容器)。接口到实现的绑定同样记录在定义表中,例如 LoggerInterface::class => FileLogger::class。
2、反射工厂
反射工厂负责在缺少显式工厂时自动构建对象:通过 ReflectionClass 读取构造函数签名,逐个参数判断类型——类类型递归调用容器的 get() 方法,标量类型回退到默认值,无法解析时抛出明确异常。PHP 8.0 引入的构造器属性提升让签名声明更紧凑,反射的读取方式保持不变。
3、三级缓存池

缓存池决定容器的运行效率,通常分为三层:
-
实例缓存:标记为单例的服务只创建一次,后续
get()直接返回同一实例,这是容器的共享语义基础。 -
定义缓存:绑定关系在注册期写入、解析期只读,避免每次解析重复归一化创建方式。
-
反射元数据缓存:
ReflectionClass实例与构造参数列表的创建涉及类结构扫描,按类名缓存后每个类只反射一次;需要跨进程共享时可再桥接 PSR-6 缓存池实现。
三、环境准备与 PSR-11 约定
运行环境要求 PHP 8.1 及以上版本,代码用到一等可调用语法与构造器属性提升。接口层面遵循 PHP-FIG 发布的 PSR-11(Container Interface)标准,该标准仅定义 get(string $id) 与 has(string $id) 两个方法,异常分为 ContainerExceptionInterface 与 NotFoundExceptionInterface 两类。通过 Composer 引入接口包:
composer require psr/container实现 PSR-11 的价值在于可互换性:任何兼容该标准的框架或组件都能直接替换此容器,业务代码不发生改动。
四、实现容器核心代码
1、注册与单例绑定
set() 负责写入定义表,singleton() 在注册的同时打上共享标记。重复注册同名服务时清除旧实例缓存,保证后续解析拿到最新定义。
<?php
declare(strict_types=1);
namespace App\Container;
use Closure;
use Psr\Container\ContainerInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
use RuntimeException;
class ContainerException extends RuntimeException implements ContainerExceptionInterface {}
class NotFoundException extends RuntimeException implements NotFoundExceptionInterface {}
final class Container implements ContainerInterface
{
/** @var array<string, mixed> 服务定义:类名、闭包或实例 */
private array $definitions = [];
/** @var array<string, object> 单例实例缓存 */
private array $instances = [];
/** @var array<string, true> 单例标记 */
private array $shared = [];
/** @var array<string, ReflectionClass> 反射元数据缓存 */
private array $reflections = [];
/** @var array<string, array<int, ReflectionParameter>> 构造签名缓存 */
private array $signatures = [];
/** @var array<int, string> 解析栈,用于循环依赖检测 */
private array $resolving = [];
public function set(string $id, mixed $concrete = null, bool $shared = false): void
{
$this->definitions[$id] = $concrete ?? $id;
if ($shared) {
$this->shared[$id] = true;
}
unset($this->instances[$id]);
}
public function singleton(string $id, mixed $concrete = null): void
{
$this->set($id, $concrete, true);
}
public function has(string $id): bool
{
return isset($this->definitions[$id]) || class_exists($id);
}
}2、反射工厂解析构造参数
get() 是解析入口:先查实例缓存,再按定义分发——闭包交给 call(),类名交给 build()。build() 内部利用反射缓存读取构造签名,逐参数解析后通过 newInstanceArgs() 完成实例化,命中单例标记的对象写入实例缓存。
public function get(string $id): mixed
{
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
$concrete = $this->definitions[$id] ?? $id;
// 已创建实例直接托管
if (is_object($concrete) && !$concrete instanceof Closure) {
return $this->instances[$id] = $concrete;
}
try {
$object = $concrete instanceof Closure
? $this->call($concrete)
: $this->build(is_string($concrete) ? $concrete : $id);
} catch (ReflectionException $e) {
throw new NotFoundException("服务 [{$id}] 未找到", 0, $e);
}
if (isset($this->shared[$id])) {
$this->instances[$id] = $object;
}
return $object;
}
private function build(string $class): object
{
$reflection = $this->reflections[$class] ??= new ReflectionClass($class);
if (!$reflection->isInstantiable()) {
throw new ContainerException(
"[{$class}] 不可实例化,接口与抽象类需要先绑定具体实现"
);
}
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return $reflection->newInstance();
}
$this->assertNotCircular($class);
$this->resolving[] = $class;
try {
$parameters = $this->signatures[$class] ??= $constructor->getParameters();
$arguments = [];
foreach ($parameters as $parameter) {
if ($parameter->isVariadic()) {
break; // 变参无显式绑定时终止注入
}
$arguments[] = $this->resolveParameter($parameter);
}
return $reflection->newInstanceArgs($arguments);
} finally {
array_pop($this->resolving);
}
}
private function resolveParameter(ReflectionParameter $parameter): mixed
{
$type = $parameter->getType();
// 类类型:递归交给容器解析
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
return $this->get($type->getName());
}
// 标量类型:回退默认值
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new ContainerException(
"无法解析参数 \${$parameter->getName()}:标量缺少默认值,联合类型不参与自动装配"
);
}3、循环依赖检测
构造器注入场景下循环依赖无法被自动满足,唯一正确的处理是快速失败。解析栈 resolving 记录当前递归链,目标类已存在于栈中即判定成环,抛出携带完整依赖链的异常,定位成本接近于零。
private function assertNotCircular(string $class): void
{
if (in_array($class, $this->resolving, true)) {
$chain = implode(' -> ', [...$this->resolving, $class]);
throw new ContainerException("检测到循环依赖:{$chain}");
}
}4、闭包工厂与参数注入
闭包定义的服务由 call() 执行。借助 ReflectionFunction,闭包声明的类型化参数同样享受自动注入,工厂函数因此可以同时依赖容器中已注册的其他服务:
public function call(Closure $factory): mixed
{
$reflection = new ReflectionFunction($factory);
$arguments = [];
foreach ($reflection->getParameters() as $parameter) {
$arguments[] = $this->resolveParameter($parameter);
}
return $factory(...$arguments);
}五、完整使用示例
以下示例覆盖接口绑定、单例、闭包工厂与自动装配四种典型场景:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use App\Container\Container;
interface LoggerInterface
{
public function log(string $message): void;
}
final class FileLogger implements LoggerInterface
{
public function __construct(private string $path = '/tmp/app.log') {}
public function log(string $message): void
{
file_put_contents($this->path, $message . PHP_EOL, FILE_APPEND);
}
}
final class UserService
{
public function __construct(
private LoggerInterface $logger,
private PDO $db,
) {}
public function register(string $name): void
{
$this->logger->log("register: {$name}");
// 数据库写入逻辑……
}
}
$container = new Container();
// 接口绑定到实现,单例共享
$container->singleton(LoggerInterface::class, FileLogger::class);
// 闭包工厂:带配置的 PDO 连接,同样注册为单例
$container->singleton(PDO::class, fn(): PDO => new PDO(
'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4',
'app',
'secret',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
));
// UserService 未注册,反射工厂自动解析 LoggerInterface 与 PDO
$userService = $container->get(UserService::class);
// 单例验证:两次解析返回同一实例
var_dump($container->get(PDO::class) === $container->get(PDO::class)); // bool(true)UserService 从未显式注册,get(UserService::class) 依然完成了整个依赖图的装配:反射工厂读取构造签名,LoggerInterface 命中定义表中的绑定,PDO 命中闭包工厂,标量参数 $path 回退到默认值。
六、缓存池的性能与内存权衡
1、反射元数据缓存的收益
ReflectionClass 的实例化涉及类结构扫描,单次开销在微秒量级,低并发下可以忽略;但在高并发服务中同一服务被解析成千上万次,无缓存的容器会反复支付这笔成本。按类名缓存反射对象与构造签名后,每个类在整个进程生命周期内只反射一次。Symfony DI 在此基础上更进一步:把解析结果编译为纯 PHP 文件,运行时完全绕开反射,这是大型项目值得参考的方向。
2、单例缓存的内存代价
单例的生命周期与容器一致。在 PHP-FPM 模型下请求结束即整体释放,无需特别关注;在 Swoole、Laravel Octane 等常驻内存模型中,单例会持续驻留内存,请求级数据(当前用户、请求 ID)一旦写入单例就会造成跨请求污染,此类状态应通过工厂每次新建,或放入协程上下文。
3、OPcache 与预加载配合
开启 OPcache 后,容器代码以共享内存字节码形态存在,省去每次请求的编译开销;PHP 7.4 起提供的 opcache.preload 可在服务启动时把容器类与定义表预热进内存,进一步压缩首个请求的解析成本。容器自身的数组结构对 OPcache 友好,无需特殊适配。
七、生产环境注意事项
-
接口必须先绑定再解析:
ReflectionClass::isInstantiable()对接口与抽象类返回 false,容器抛出异常提示绑定,避免含糊的运行时错误。 -
标量参数不猜测:配置值应通过闭包工厂或参数绑定显式提供,容器只对带默认值的标量做回退,拒绝“猜配置”的隐式行为。
-
联合类型不参与自动装配:
A|B形式的参数无法唯一确定目标,按异常处理并在提示中说明,与 PHP-DI 的默认行为一致。 -
检测循环依赖后快速失败:构造器注入成环没有合法解,应修改设计(如引入中间层或改为 setter 注入),容器职责是给出清晰的依赖链报告。
-
长生命周期进程注意清理:常驻内存模型中,请求结束可调用清理方法清空
instances与resolving,防止内存无界增长。

