一句话

依赖注入(Dependency Injection)= 一个类不自己创建依赖,而是由外部传入。
IoC 容器 = 自动帮你完成”传入”这个动作的工厂 + 注册表。

一、问题:为什么不能自己 new

1
2
3
4
5
6
class OrderService {
private MysqlOrderRepo $repo;
public function __construct() {
$this->repo = new MysqlOrderRepo(new PDO(...)); // ❌
}
}

毛病:

  1. 不可换实现 —— 想换 Redis?改源码
  2. 不可测 —— 单元测试想 mock 数据库?改不动
  3. 强耦合 —— OrderService 必须知道 PDO 的连接串
  4. 依赖隐藏 —— 看构造器看不出它依赖谁

二、三种注入方式

1. 构造器注入(推荐)

1
2
3
4
class OrderService {
public function __construct(private OrderRepo $repo) {}
}
$svc = new OrderService(new MysqlOrderRepo($pdo));

最佳实践:依赖必填、对象不可变、看签名一目了然。

2. Setter 注入

1
2
3
4
class OrderService {
private OrderRepo $repo;
public function setRepo(OrderRepo $repo): void { $this->repo = $repo; }
}

适用:可选依赖、运行时切换。缺点:对象创建后状态不完整。

3. 接口注入 / 属性注入

1
2
3
class OrderService {
#[Inject] public OrderRepo $repo; // PHP 8 attribute,框架解析
}

适用:框架内部魔法。缺点:依赖魔法、IDE 补全难。

三、面向接口编程

DI 真正的价值要配合接口

1
2
3
4
5
6
7
8
9
10
interface OrderRepo {
public function find(int $id): ?Order;
}
class MysqlOrderRepo implements OrderRepo { /*...*/ }
class RedisOrderRepo implements OrderRepo { /*...*/ }
class FakeOrderRepo implements OrderRepo { /*for test*/ }

class OrderService {
public function __construct(private OrderRepo $repo) {} // 类型是接口!
}

测试时:

1
$svc = new OrderService(new FakeOrderRepo());   // 不碰数据库就能测

四、IoC 容器:自动注入

手动 new 几十层依赖太累,IoC 容器替你做:

1
2
$container->bind(OrderRepo::class, MysqlOrderRepo::class);
$svc = $container->make(OrderService::class); // 自动 new MysqlOrderRepo + PDO

30 行手写一个容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Container {
private array $bindings = [];

public function bind(string $abstract, string|callable $concrete): void {
$this->bindings[$abstract] = $concrete;
}

public function make(string $abstract): object {
$concrete = $this->bindings[$abstract] ?? $abstract;
if (is_callable($concrete)) return $concrete($this);

$ref = new ReflectionClass($concrete);
$ctor = $ref->getConstructor();
if (!$ctor) return new $concrete();

$args = [];
foreach ($ctor->getParameters() as $p) {
$type = $p->getType();
if ($type && !$type->isBuiltin()) {
$args[] = $this->make($type->getName()); // 递归注入
}
}
return $ref->newInstanceArgs($args);
}
}

// 用法
$c = new Container();
$c->bind(OrderRepo::class, MysqlOrderRepo::class);
$svc = $c->make(OrderService::class);

Laravel / Symfony 的容器本质就是这套,加了单例、上下文绑定、循环依赖检测、属性注入等增强。

五、DI vs Service Locator

很多人混淆两者:

1
2
3
4
5
6
7
8
9
10
11
// DI(推荐):依赖在构造器里声明
class OrderService {
public function __construct(private OrderRepo $repo) {}
}

// Service Locator(反模式):依赖藏在内部
class OrderService {
public function process() {
$repo = ServiceLocator::get(OrderRepo::class); // ❌
}
}

Service Locator 把依赖隐藏到方法内部,等于没做 DI

六、什么时候不用 DI

  • 极小脚本 / 一次性工具
  • 全是静态工具函数(无状态)
  • 性能极致敏感的热路径(容器有反射开销,但生产环境一般已编译/缓存掉)

参考