PHP依赖注入在DDD中怎么用:从领域模型到代码落地的完整指南
目录导读
为什么DDD需要依赖注入?
领域驱动设计(DDD)强调领域模型的纯粹性与独立性,要求领域层(Domain Layer)不依赖基础设施(如数据库、缓存、外部API),但在实际业务中,领域对象常常需要查询持久化数据或执行外部操作——这看似矛盾。

依赖注入(DI)正是解决这一矛盾的核心模式,通过将依赖的创建与生命周期管理交由外部容器(Container),领域层仅需声明接口依赖,即可保持纯净。
DI让领域层“不知道”谁实现了接口,但能“拿到”实现 —— 这正好满足DDD的“领域层零外部依赖”原则。
依赖注入的核心概念与DDD的映射关系
| DI概念 | DDD中的对应角色 | 作用 |
|---|---|---|
| 容器 (Container) | 应用层/基础设施层 | 管理对象创建、生命周期、注入时机 |
| 接口 (Interface) | 领域接口 (Repository接口等) | 定义契约,不暴露实现细节 |
| 具体实现类 | 基础设施层实现类 | 如MysqlOrderRepository、RedisCache |
| 注入点 | 领域服务、应用服务、工厂 | 通过构造函数、Setter、属性注入 |
关键原则:领域层只定义接口(如 OrderRepositoryInterface),基础设施层定义实现(如 MysqlOrderRepository),容器在应用层负责将实现注入到领域服务中。
PHP中实现依赖注入的三种方式
1 手动注入(传统方式)
class CreateOrderService {
private OrderRepositoryInterface $repo;
// 构造函数注入
public function __construct(OrderRepositoryInterface $repo) {
$this->repo = $repo;
}
}
优点:简单直观,无外部依赖
缺点:每次调用时需手动实例化,耦合度高
2 使用容器(推荐)
以PHP-DI、Laravel容器、Symfony DI为例:
// 在容器中绑定接口与实现
$container->set(OrderRepositoryInterface::class,
DI\autowire(MysqlOrderRepository::class));
// 容器自动解析
$orderService = $container->get(CreateOrderService::class);
优点:自动递归解析依赖,支持延迟加载
缺点:引入容器框架,需学习配置语法
3 基于属性注入(谨慎使用)
class OrderProcessor {
#[Inject]
private OrderRepositoryInterface $repo;
}
优点:代码更简洁
缺点:隐蔽依赖,破坏可测试性,建议仅用于框架内置组件
在DDD分层架构中的具体应用
1 应用层:依赖注入的“控制中心”
应用服务是注入的入口,负责编排领域服务与基础设施:
// app/Application/OrderApplicationService.php
class OrderApplicationService {
public function __construct(
private OrderService $orderService, // 领域服务
private OrderRepositoryInterface $repo, // 依赖注入
private EventDispatcherInterface $dispatcher
) {}
public function createOrder(array $data): Order {
$order = $this->orderService->create($data);
$this->repo->save($order);
$this->dispatcher->dispatch(new OrderCreated($order));
return $order;
}
}
2 领域层:只依赖接口,不依赖实现
// domain/Service/OrderService.php
class OrderService {
public function __construct(
private ProductRepositoryInterface $productRepo, // 接口注入
private DomainEventBus $eventBus
) {}
public function createOrder(array $data): Order {
$product = $this->productRepo->findById($data['productId']);
// 领域逻辑...
return new Order($product, $data['quantity']);
}
}
3 基础设施层:实现接口,暴露给容器
// infrastructure/Persistence/MysqlOrderRepository.php
class MysqlOrderRepository implements OrderRepositoryInterface {
public function __construct(private PDO $pdo) {} // PDO由容器注入
public function save(Order $order): void {
// 使用 $this->pdo 执行SQL
}
}
4 容器配置实例(使用PHP-DI)
// config/di.php
return [
OrderRepositoryInterface::class => DI\autowire(MysqlOrderRepository::class)
->constructorParameter('pdo', DI\get('db.pdo')),
OrderService::class => DI\autowire()
->constructorParameter('eventBus', DI\get(DomainEventBus::class)),
'db.pdo' => function () {
return new PDO('mysql:host=localhost;dbname=shop', 'user', 'pass');
},
];
实战案例:订单领域服务注入仓库
场景:创建订单时需要校验库存,但库存数据存储在外部库存服务(Infrastructure)
错误做法:领域服务直接new库存服务
class OrderService {
public function createOrder(Product $product): void {
$stockService = new ExternalStockService(); // 强依赖!
if (!$stockService->checkStock($product->getId())) {
throw new InsufficientStockException();
}
}
}
DDD+DI正确做法:
-
定义接口(领域层):
interface ProductStockInterface { public function checkStock(string $productId): bool; } -
实现接口(基础设施层):
class ExternalProductStock implements ProductStockInterface { public function __construct(private HttpClient $http) {} public function checkStock(string $productId): bool { $response = $this->http->get("/stock/{$productId}"); return $response['available'] > 0; } } -
注入到领域服务(容器自动完成):
class OrderService { public function __construct( private ProductStockInterface $stockChecker // 仅依赖接口 ) {} public function createOrder(Product $product): void { if (!$this->stockChecker->checkStock($product->getId())) { throw new InsufficientStockException(); } // 创建订单... } } -
测试时替换(注入Mock实现):
$mockStock = new class implements ProductStockInterface { public function checkStock(string $productId): bool { return false; // 模拟库存不足 } }; $service = new OrderService($mockStock);
常见问题与陷阱
问题1:“领域对象(Entity)是否应该使用DI?”
不建议,实体是状态载体,不应包含业务逻辑中的依赖,应将逻辑抽取到领域服务中。
问题2:“容器性能是否受影响?”
初始化影响可忽略,现代容器(PHP-DI、Laravel Container)支持延迟加载,只在实际调用时实例化对象。
问题3:“如何避免‘注入地狱’?”
当构造函数参数超过5个时,考虑:
- 抽取为多个更细粒度领域服务
- 使用Facade模式(不推荐,破坏DDD)
- 使用构建器模式(Builder)
问题4:“非容器环境(如传统框架)怎么办?”
可以手工实现简单容器:
class SimpleContainer {
private array $bindings = [];
public function set(string $abstract, callable $factory): void {
$this->bindings[$abstract] = $factory;
}
public function get(string $abstract): object {
return ($this->bindings[$abstract])();
}
}
Q&A精选
Q: 依赖注入和依赖反转(DIP)有什么区别?
A: DIP是原则,“高层模块不应依赖低层模块,两者都应依赖抽象”,DI是实现该原则的一种具体技术,在DDD中,领域层只定义接口,正是遵循DIP;容器负责注入具体实现,即DI。
Q: 在PHP中,是否必须用容器才能实现DI?
A: 不一定,只要通过构造函数/Setter传递依赖,就实现了DI,但容器能自动解析依赖树、管理生命周期,更适合大型DDD项目。
Q: DD与DI结合时,测试怎么写?
A: 单元测试时手动注入Mock对象;集成测试时使用容器注入真实基础设施(如内存数据库)。
Q: 如果基础设施层抛异常,领域层如何感知?
A: 在基础设施层捕获底层异常(如PDOException),转换为领域层可理解的领域异常(如 PersistenceException),然后抛出,领域层只处理领域异常。
Q: 能否使用服务定位器(Service Locator)代替DI?
A: 可以,但不推荐,服务定位器将依赖拉取到领域层,破坏封装性,DI能更清晰地表达“领域层需要什么”,而不是“领域层去哪里拿”。
PHP依赖注入与DDD的结合,本质上是“控制反转(IoC)思想在领域驱动设计中的落地”,通过容器自动注入接口实现,领域层得以保持纯净、可测试、可演进,正确使用DI的关键在于:领域层只依赖接口,基础设施层负责实现,应用层负责编排,从简单的构造函数注入到成熟的容器管理,选择适合项目规模的方案即可。