PHP依赖注入在DDD中怎么用

wen PHP项目 1

PHP依赖注入在DDD中怎么用:从领域模型到代码落地的完整指南

目录导读

  1. 为什么DDD需要依赖注入?
  2. 依赖注入的核心概念与DDD的映射关系
  3. PHP中实现依赖注入的三种方式
  4. 在DDD分层架构中的具体应用
  5. 实战案例:订单领域服务注入仓库
  6. 常见问题与陷阱
  7. Q&A精选

为什么DDD需要依赖注入?

领域驱动设计(DDD)强调领域模型的纯粹性与独立性,要求领域层(Domain Layer)不依赖基础设施(如数据库、缓存、外部API),但在实际业务中,领域对象常常需要查询持久化数据或执行外部操作——这看似矛盾。

PHP依赖注入在DDD中怎么用

依赖注入(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正确做法

  1. 定义接口(领域层):

    interface ProductStockInterface {
     public function checkStock(string $productId): bool;
    }
  2. 实现接口(基础设施层):

    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;
     }
    }
  3. 注入到领域服务(容器自动完成):

    class OrderService {
     public function __construct(
         private ProductStockInterface $stockChecker // 仅依赖接口
     ) {}
     public function createOrder(Product $product): void {
         if (!$this->stockChecker->checkStock($product->getId())) {
             throw new InsufficientStockException();
         }
         // 创建订单...
     }
    }
  4. 测试时替换(注入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的关键在于:领域层只依赖接口,基础设施层负责实现,应用层负责编排,从简单的构造函数注入到成熟的容器管理,选择适合项目规模的方案即可。

上一篇Laravel邮件服务是基础设施吗

下一篇当前分类已是最新一篇

抱歉,评论功能暂时关闭!