1. 引言
在 PHP 项目开发中,随着业务逻辑不断膨胀,类与类之间的依赖关系会变得越来越复杂。如果直接在类内部 new 出依赖对象,代码就会耦合在一起,难以测试和维护。依赖注入(Dependency Injection,DI)与控制反转(Inversion of Control,IoC)正是为了解决这一问题而生的设计思想。
本文将从最基础的概念讲起,逐步手写一个轻量级的依赖注入容器,覆盖手动注入、反射解析构造函数依赖、单例注册、自动装配以及循环依赖检测等核心能力。读完本文,你将彻底理解依赖注入容器的工作原理,并能独立实现一个可用的容器。
2. 控制反转与依赖注入基础概念
2.1 什么是控制反转
控制反转是一种设计原则,核心思想是:将对象创建和依赖管理的控制权从调用方手中反转给容器或框架。传统写法中,类自己决定依赖谁、何时创建依赖;而 IoC 模式下,类只声明自己需要什么,由外部容器负责提供。
2.2 什么是依赖注入
依赖注入是控制反转的一种具体实现方式,指通过构造函数、Setter 方法或属性等方式,将依赖对象传递给使用方。常见的注入方式有三种:
- 构造函数注入:依赖在对象创建时通过构造参数传入,最推荐使用。
- Setter 注入:通过公开的 setter 方法注入依赖,适合可选依赖。
- 属性注入:直接给公开属性赋值,简单但破坏封装。
2.3 为什么需要依赖注入容器
当项目规模变大,手动管理依赖会变得繁琐。依赖注入容器负责统一管理对象的创建、配置和生命周期,让开发者只需关注业务逻辑,而无需关心依赖如何组装。
3. 手动注入 vs 容器管理
3.1 手动注入的写法
手动注入是指开发者自己创建依赖对象并传入。这种方式在小项目中足够,但存在明显问题:
php
<?php
class Database
{
public function query(string $sql): array
{
// 模拟查询
return ['result' => $sql];
}
}
class UserRepository
{
private Database $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function findUser(int $id): array
{
return $this->db->query("SELECT * FROM users WHERE id = {$id}");
}
}
// 手动创建依赖
$db = new Database();
$repo = new UserRepository($db);
手动注入的缺点:
- 依赖较多时,创建代码冗长且重复。
- 依赖关系变化时,需要修改所有创建点。
- 难以统一管理单例和生命周期。
3.2 容器管理的优势
使用容器后,上述创建过程被简化为一行代码:
php
$repo = $container->get(UserRepository::class);
容器自动完成依赖的解析与组装,开发者无需关心 Database 何时创建、如何传入。容器还支持单例注册、延迟加载和统一配置,让依赖管理变得集中且可控。
4. 从零实现一个依赖注入容器
4.1 容器的基础结构
我们先定义一个最简容器类,支持绑定和获取两个核心操作:
php
<?php
class Container
{
private array $bindings = [];
/**
* 注册一个绑定
*/
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
/**
* 从容器中解析一个实例
*/
public function get(string $abstract)
{
if (!isset($this->bindings[$abstract])) {
throw new RuntimeException("No binding for {$abstract}");
}
$factory = $this->bindings[$abstract];
return $factory($this);
}
}
使用方式:
php
$container = new Container();
$container->bind(Database::class, function () {
return new Database();
});
$container->bind(UserRepository::class, function (Container $c) {
return new UserRepository($c->get(Database::class));
});
$repo = $container->get(UserRepository::class);
4.2 支持单例注册
单例模式保证同一个类在容器中只被实例化一次。我们增加 singleton 方法和缓存机制:
php
<?php
class Container
{
private array $bindings = [];
private array $singletons = [];
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
public function singleton(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
$this->singletons[$abstract] = true;
}
public function get(string $abstract)
{
// 单例已实例化,直接返回缓存
if (isset($this->singletons[$abstract]) && isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
if (!isset($this->bindings[$abstract])) {
throw new RuntimeException("No binding for {$abstract}");
}
$factory = $this->bindings[$abstract];
$instance = $factory($this);
// 单例绑定则缓存实例
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
}
4.3 反射解析构造函数依赖
手动注册每个类的工厂函数仍然繁琐。利用 PHP 的反射机制,容器可以自动分析类的构造函数参数,并递归解析每个参数对应的依赖,这就是自动装配。
php
<?php
class Container
{
private array $bindings = [];
private array $singletons = [];
private array $instances = [];
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
public function singleton(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
$this->singletons[$abstract] = true;
}
public function get(string $abstract)
{
// 单例缓存
if (isset($this->singletons[$abstract]) && isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// 有显式绑定则使用工厂
if (isset($this->bindings[$abstract])) {
$factory = $this->bindings[$abstract];
$instance = $factory($this);
} else {
// 否则通过反射自动装配
$instance = $this->resolve($abstract);
}
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
/**
* 通过反射解析类的构造函数依赖
*/
private function resolve(string $class)
{
$reflection = new ReflectionClass($class);
if (!$reflection->isInstantiable()) {
throw new RuntimeException("Class {$class} is not instantiable");
}
$constructor = $reflection->getConstructor();
// 没有构造函数,直接实例化
if ($constructor === null) {
return new $class();
}
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
// 参数有类类型提示,递归解析
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$dependencies[] = $this->get($type->getName());
} else {
// 基本类型参数,尝试取默认值
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new RuntimeException(
"Cannot resolve parameter \${$parameter->getName()} of {$class}"
);
}
}
}
return $reflection->newInstanceArgs($dependencies);
}
}
现在无需手动注册 UserRepository,容器会自动解析:
php
$container = new Container();
$container->singleton(Database::class, function () {
return new Database();
});
// 自动装配,无需注册 UserRepository
$repo = $container->get(UserRepository::class);
5. 循环依赖检测
5.1 什么是循环依赖
当两个类互相依赖时,就会产生循环依赖。例如 A 的构造函数需要 B,而 B 的构造函数又需要 A。如果不加处理,容器会陷入无限递归,最终导致栈溢出。
5.2 检测与报错
我们在容器中维护一个解析栈,当发现某个类正在解析中又被重复请求时,立即抛出异常:
php
<?php
class Container
{
private array $bindings = [];
private array $singletons = [];
private array $instances = [];
private array $resolving = [];
public function get(string $abstract)
{
// 检测循环依赖
if (isset($this->resolving[$abstract])) {
$chain = implode(' -> ', array_keys($this->resolving)) . ' -> ' . $abstract;
throw new RuntimeException("Circular dependency detected: {$chain}");
}
if (isset($this->singletons[$abstract]) && isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$this->resolving[$abstract] = true;
try {
if (isset($this->bindings[$abstract])) {
$factory = $this->bindings[$abstract];
$instance = $factory($this);
} else {
$instance = $this->resolve($abstract);
}
} finally {
unset($this->resolving[$abstract]);
}
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
private function resolve(string $class)
{
$reflection = new ReflectionClass($class);
if (!$reflection->isInstantiable()) {
throw new RuntimeException("Class {$class} is not instantiable");
}
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return new $class();
}
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$dependencies[] = $this->get($type->getName());
} else {
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new RuntimeException(
"Cannot resolve parameter \${$parameter->getName()} of {$class}"
);
}
}
}
return $reflection->newInstanceArgs($dependencies);
}
}
测试循环依赖检测:
php
class A
{
public function __construct(B $b) {}
}
class B
{
public function __construct(A $a) {}
}
$container = new Container();
try {
$container->get(A::class);
} catch (RuntimeException $e) {
echo $e->getMessage(); // Circular dependency detected: A -> B -> A
}
6. 完整容器示例与测试
下面给出一个整合了绑定、单例、自动装配和循环检测的完整容器,并附上测试用例:
php
<?php
class Container
{
private array $bindings = [];
private array $singletons = [];
private array $instances = [];
private array $resolving = [];
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
public function singleton(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
$this->singletons[$abstract] = true;
}
public function get(string $abstract)
{
if (isset($this->resolving[$abstract])) {
$chain = implode(' -> ', array_keys($this->resolving)) . ' -> ' . $abstract;
throw new RuntimeException("Circular dependency detected: {$chain}");
}
if (isset($this->singletons[$abstract]) && isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
$this->resolving[$abstract] = true;
try {
if (isset($this->bindings[$abstract])) {
$instance = $this->bindings[$abstract]($this);
} else {
$instance = $this->resolve($abstract);
}
} finally {
unset($this->resolving[$abstract]);
}
if (isset($this->singletons[$abstract])) {
$this->instances[$abstract] = $instance;
}
return $instance;
}
private function resolve(string $class)
{
$reflection = new ReflectionClass($class);
if (!$reflection->isInstantiable()) {
throw new RuntimeException("Class {$class} is not instantiable");
}
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return new $class();
}
$dependencies = [];
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$dependencies[] = $this->get($type->getName());
} else {
if ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new RuntimeException(
"Cannot resolve parameter \${$parameter->getName()} of {$class}"
);
}
}
}
return $reflection->newInstanceArgs($dependencies);
}
}
// ---------- 测试用例 ----------
class Logger
{
public function log(string $msg): void
{
echo "[LOG] {$msg}\n";
}
}
class Mailer
{
private Logger $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function send(string $to): void
{
$this->logger->log("Sending mail to {$to}");
}
}
$container = new Container();
// 注册 Logger 为单例
$container->singleton(Logger::class, function () {
return new Logger();
});
// Mailer 无需注册,自动装配
$mailer1 = $container->get(Mailer::class);
$mailer2 = $container->get(Mailer::class);
// 验证单例:两次获取的 Logger 是同一个实例
$logger1 = $container->get(Logger::class);
$logger2 = $container->get(Logger::class);
var_dump($logger1 === $logger2); // bool(true)
$mailer1->send('user@example.com');
7. 总结
本文从控制反转与依赖注入的基本概念出发,逐步实现了一个功能完整的 PHP 依赖注入容器。核心要点回顾:
- 控制反转将依赖创建的控制权交给容器,降低类间耦合。
- 手动注入适合小项目,但依赖复杂时维护成本高。
- 容器管理通过绑定、单例和自动装配,让依赖组装集中且可复用。
- 反射自动装配 利用
ReflectionClass解析构造函数参数,递归解析依赖。 - 循环依赖检测通过解析栈标记正在解析的类,遇到重复请求立即抛出异常。
实际项目中,你可以直接使用成熟的容器实现(如 PHP-DI、Laravel 的容器),但理解其底层原理,能帮助你在遇到复杂依赖场景时更快定位问题,也能让你写出更优雅、可维护的代码。