深度解析PHP项目中的聚合根与仓储:DDD落地的核心实践
目录导读
聚合根:领域模型的守护者
在领域驱动设计(DDD)中,聚合根是领域模型的核心概念,它是一组相关对象的根实体,负责维护聚合内部的业务规则和一致性边界,在PHP项目中,聚合根通常是一个类,它持有对子实体和值对象的引用,并对外暴露业务方法。

聚合根的关键特性:
- 唯一标识:每个聚合根都有全局唯一的ID,通常是UUID或自增主键
- 边界控制:外部只能通过聚合根访问聚合内的对象,不能直接操作子实体
- 事务一致性:聚合内的所有变更必须在一次事务中完成
- 业务完整性:聚合根保证业务规则不被破坏
一个“订单”聚合根包含订单项、收货地址等信息,修改订单时,必须通过订单根对象的方法来完成,确保总价计算正确、库存扣减一致。
在PHP项目中,聚合根的典型实现如下:
class Order {
private string $orderId;
private string $status;
private array $items; // OrderItem 实体集合
private Address $shippingAddress; // 值对象
public function addItem(Product $product, int $quantity): void {
// 业务规则:检查库存、计算价格
$this->items[] = new OrderItem($product->getId(), $quantity);
}
public function submit(): void {
if ($this->status !== 'draft') {
throw new \DomainException('只有草稿订单可以提交');
}
$this->status = 'submitted';
// 触发领域事件
}
}
仓储:数据持久化的桥梁
仓储是对聚合根进行持久化和检索的抽象层,它屏蔽了数据存储的细节,让领域层专注于业务逻辑,仓储接口定义在领域层,实现则在基础设施层。
仓储的核心职责:
- 存储聚合根:将聚合根的状态保存到数据库、缓存或文件系统
- 检索聚合根:根据ID或其他条件恢复聚合根
- 删除聚合根:从持久化存储中移除聚合根
- 管理工作单元:通常与ORM或事务管理器配合
在PHP中,仓储接口与实现的示例:
// 领域层接口
interface OrderRepository {
public function findById(string $orderId): ?Order;
public function save(Order $order): void;
public function delete(string $orderId): void;
}
// 基础设施层实现(使用Doctrine ORM)
class DoctrineOrderRepository implements OrderRepository {
private EntityManager $em;
public function findById(string $orderId): ?Order {
return $this->em->find(Order::class, $orderId);
}
public function save(Order $order): void {
$this->em->persist($order);
$this->em->flush();
}
}
注意:仓储只操作聚合根,不直接处理子实体,子实体的生命周期由聚合根管理。
聚合根与仓储的协作机制
聚合根和仓储是DDD中一对紧密合作的模式,它们的协作遵循以下流程:
- 获取聚合根:客户端通过仓储的
findById方法获取聚合根实例 - 业务操作:对聚合根调用业务方法,修改其内部状态
- 持久化:调用仓储的
save方法,将聚合根保存回数据存储
这种设计带来的好处:
- 解耦业务逻辑与数据访问:业务代码不关心SQL、缓存或ORM
- 单一职责:聚合根负责业务,仓储负责持久化
- 可测试性:可以轻松模拟仓储进行单元测试
在PHP项目中,常见的实践是使用服务层来编排这些操作:
class OrderService {
private OrderRepository $orderRepo;
public function submitOrder(string $orderId): void {
$order = $this->orderRepo->findById($orderId);
if (!$order) throw new \RuntimeException('订单不存在');
$order->submit();
$this->orderRepo->save($order);
}
}
PHP实战:从理论到代码
1 定义聚合根类
选择正确的聚合根是设计的关键,以下是一些判断标准:
- 该对象是否拥有唯一的不变性边界?
- 删除它时,是否也意味着删除其关联对象?
- 它是否代表一个主要的业务概念?
2 实现仓储接口
避免在仓储中混入查询逻辑,如果需要复杂查询,应使用独立的数据查询服务(例如Specification模式)。
3 使用依赖注入
通过PHP的依赖注入容器(如PHP-DI或Symfony DI)注入仓储实例,保持代码灵活。
4 处理事务
仓储的save方法应该保证原子性,在Laravel中,可以这样实现:
public function save(Order $order): void {
DB::transaction(function () use ($order) {
DB::table('orders')->upsert(/* 订单数据 */);
// 更新子实体...
});
}
常见问题与最佳实践
常见陷阱:
- 仓储调用其他仓储:一个仓储不应直接引用另一个仓储,应通过领域服务协调
- 直接修改子实体集合:外部代码不应直接操作
$order->items,应通过订单根的方法 - 事务过大:坚持一个请求只修改一个聚合根的原则
最佳实践:
- 使用CQRS分离读写模型,避免仓储承载查询职责
- 在聚合根中注册领域事件,实现跨聚合的最终一致性
- 选择成熟的ORM(如Doctrine)简化仓储实现
问答环节
Q1:聚合根和实体有什么区别?
A:实体有唯一标识,但聚合根是实体的一个子集,它是对外访问的入口,普通实体(如订单项)只能通过聚合根访问。
Q2:仓储是否必须与ORM绑定?
A:不必,仓储接口是抽象,可以使用原始数据库驱动、ORM(Doctrine、Eloquent)、文件存储甚至外部API实现。
Q3:如何确定聚合根的大小?
A:遵循“一个用例修改一个聚合”的原则,如果两个对象经常一起变更,它们很可能属于同一个聚合根。
Q4:PHP框架中如何实现仓储?
A:在Laravel中,可以创建Repository类注入Model;在Symfony中,结合Doctrine的Repository模式实现,关键是保持接口层在领域层,实现层在基础设施层。
聚合根与仓储是构建可维护PHP应用的基础,通过合理的领域模型设计,我们能够摆脱传统CRUD的泥潭,让业务逻辑清晰、可扩展,聚合根是业务的核心,仓储是数据的守门人,两者协同才能实现真正的领域驱动持久化。