### [手写 PHP 依赖注入容器:反射工厂与缓存池](https://www.huociguo.com/article/176) **Published:** 2026-07-17T09:10:01 **Author:** 米了 **Excerpt:** 依赖注入容器的本质是一台对象装配机。基于 PHP 反射 API 读取构造函数签名,即可完成参数的自动解析与递归注入;再配合由实例缓存、定义缓存、反射元数据缓存组成的三级缓存池,可将反射开销压缩到可忽略区间。以下内容基于 PHP 8.1 与 > 依赖注入容器的本质是一台对象装配机。基于 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、三级缓存池** ![](https://api.huociguo.com/wp-content/uploads/2026/07/php-di-container-reflection-cache-17091692af-1.png "php-di-container-reflection-cache-17091692af-1") 缓存池决定容器的运行效率,通常分为三层: - **实例缓存**:标记为单例的服务只创建一次,后续 `get()` 直接返回同一实例,这是容器的共享语义基础。 - **定义缓存**:绑定关系在注册期写入、解析期只读,避免每次解析重复归一化创建方式。 - **反射元数据缓存**:`ReflectionClass` 实例与构造参数列表的创建涉及类结构扫描,按类名缓存后每个类只反射一次;需要跨进程共享时可再桥接 PSR-6 缓存池实现。 ## **三、环境准备与 PSR-11 约定** 运行环境要求 PHP 8.1 及以上版本,代码用到一等可调用语法与构造器属性提升。接口层面遵循 PHP-FIG 发布的 PSR-11(Container Interface)标准,该标准仅定义 `get(string $id)` 与 `has(string $id)` 两个方法,异常分为 `ContainerExceptionInterface` 与 `NotFoundExceptionInterface` 两类。通过 Composer 引入接口包: ```bash composer require psr/container ``` 实现 PSR-11 的价值在于可互换性:任何兼容该标准的框架或组件都能直接替换此容器,业务代码不发生改动。 ## **四、实现容器核心代码** ### **1、注册与单例绑定** `set()` 负责写入定义表,`singleton()` 在注册的同时打上共享标记。重复注册同名服务时清除旧实例缓存,保证后续解析拿到最新定义。 ```php 服务定义:类名、闭包或实例 */ private array $definitions = []; /** @var array 单例实例缓存 */ private array $instances = []; /** @var array 单例标记 */ private array $shared = []; /** @var array 反射元数据缓存 */ private array $reflections = []; /** @var array> 构造签名缓存 */ private array $signatures = []; /** @var array 解析栈,用于循环依赖检测 */ 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()` 完成实例化,命中单例标记的对象写入实例缓存。 ```php 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` 记录当前递归链,目标类已存在于栈中即判定成环,抛出携带完整依赖链的异常,定位成本接近于零。 ```php 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`,闭包声明的类型化参数同样享受自动注入,工厂函数因此可以同时依赖容器中已注册的其他服务: ```php public function call(Closure $factory): mixed { $reflection = new ReflectionFunction($factory); $arguments = []; foreach ($reflection->getParameters() as $parameter) { $arguments[] = $this->resolveParameter($parameter); } return $factory(...$arguments); } ``` ## **五、完整使用示例** 以下示例覆盖接口绑定、单例、闭包工厂与自动装配四种典型场景: ```php 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`,防止内存无界增长。 **Tags:** DI容器, PHP依赖注入, PSR-11, 反射工厂, 缓存池 **Categories:** PHP教程 ---