PHP项目聚合根与仓储

wen PHP项目 2

深度解析PHP项目中的聚合根与仓储:DDD落地的核心实践

目录导读

  1. 聚合根:领域模型的守护者
  2. 仓储:数据持久化的桥梁
  3. 聚合根与仓储的协作机制
  4. PHP实战:从理论到代码
  5. 常见问题与最佳实践
  6. 问答环节

聚合根:领域模型的守护者

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

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';
        // 触发领域事件
    }
}

仓储:数据持久化的桥梁

仓储是对聚合根进行持久化和检索的抽象层,它屏蔽了数据存储的细节,让领域层专注于业务逻辑,仓储接口定义在领域层,实现则在基础设施层。

仓储的核心职责:

  1. 存储聚合根:将聚合根的状态保存到数据库、缓存或文件系统
  2. 检索聚合根:根据ID或其他条件恢复聚合根
  3. 删除聚合根:从持久化存储中移除聚合根
  4. 管理工作单元:通常与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中一对紧密合作的模式,它们的协作遵循以下流程:

  1. 获取聚合根:客户端通过仓储的findById方法获取聚合根实例
  2. 业务操作:对聚合根调用业务方法,修改其内部状态
  3. 持久化:调用仓储的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(/* 订单数据 */);
        // 更新子实体...
    });
}

常见问题与最佳实践

常见陷阱:

  1. 仓储调用其他仓储:一个仓储不应直接引用另一个仓储,应通过领域服务协调
  2. 直接修改子实体集合:外部代码不应直接操作$order->items,应通过订单根的方法
  3. 事务过大:坚持一个请求只修改一个聚合根的原则

最佳实践:

  • 使用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的泥潭,让业务逻辑清晰、可扩展,聚合根是业务的核心,仓储是数据的守门人,两者协同才能实现真正的领域驱动持久化。

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