becklyn/ddd-core

PHP的DDD框架

4.0.0 2022-08-25 11:34 UTC

This package is auto-updated.

Last update: 2024-08-25 16:03:26 UTC


README

becklyn/ddd-core是一套用于开发领域驱动设计(DDD)、事件存储和CQRS的软件组件。它包括以下支持:

  • 实体身份
  • 领域事件及其处理
  • 事件存储和事件源
  • 事务处理
  • 命令处理

组件旨在简化以下工作流程:

  • 通过命令总线分派一个命令
  • 这会启动一个事务,其中命令处理器处理命令并在聚合或执行领域服务上执行所需操作
  • 如果发生未处理的异常,则回滚事务并丢弃所有更改
  • 否则,提交事务,收集由聚合或领域服务引发的事件,将它们持久化到事件存储中,并通过事件总线分派
  • 事件订阅者监听事件,并在其他聚合需要修改或需要执行其他类型的操作时分派新命令
  • 重复“核心事务循环”,直到所有事件都得到解决且没有新的命令被启动,换句话说,直到最终一致性达成。

用法

becklyn/ddd-core主要提供抽象类和接口作为组件,以促进独立于任何特定技术基础设施的使用。我们选择的是Symfony与Doctrine和SimpleBus,并在becklyn/ddd-doctrine-bridge和becklyn/ddd-symfony-bridge库中提供了将这些组件整合在一起的实施方案。如果您希望使用其他技术,您将需要提供自己的事件存储、事件、事务和命令处理实现。

核心循环

以下是关于如何实现becklyn/ddd-core工作流程的更详细解释

  • 通过CommandBus分派命令。命令是您自己创建的普通PHP类,基本上是DTO,它们应包含执行所需操作所需的所有数据。至少,它们应包含正在操作的聚合的身份。
  • CommandBus应将命令路由到其相应的处理器。由becklyn/ddd-symfony-bridge提供的实现会在正确配置的Symfony应用程序内自动完成此操作。否则,您将需要自行确保这一点。
  • 在一个扩展抽象CommandHandler类的处理器中处理命令
    • 每个命令类必须恰好有一个处理器类。
    • 您的处理器应该有一个接受命令作为其参数的公共方法,并且该方法应调用CommandHandler::handleCommand
    • handleCommand将调用抽象的execute方法,您应在该方法中实现您的命令处理逻辑
      • 加载实现EventProvider接口的聚合(我们建议从存储库中加载)。
      • 在聚合上执行操作,在其中引发领域事件(您可以使用聚合中的EventProviderCapabilitiesEventSourcedProviderCapabilities特性来简化此操作)。
      • execute方法返回聚合。
    • CommandHandler将取消排队聚合从execute返回的事件,将它们注册到EventRegistry中,并通过TransactionManager提交事务。
    • 或者,可以从execute调用领域服务。在这种情况下,服务应使用EventRegistry取消排队和注册受影响的聚合引发的事件,并且CommandHandler::execute应返回null。
  • TransactionManager::commit 应该处理任何持久化问题,并调用 EventManager::flush。同样,TransactionManager::rollback 应该丢弃所有更改并调用 EventManager::clear。becklyn/ddd-doctrine-bridge 提供了一个 Doctrine 实现。
  • 刷新 EventManager 会收集 EventRegistry 注册的所有事件并通过 EventBus 分发。清除 EventManager 只是简单地丢弃所有事件。
  • EventBus 应将事件分发到订阅了这些事件的任何订阅者。becklyn/ddd-symfony-bridge 提供了一个 Symfony/SimpleBus 实现,它可以在正确配置的 Symfony 应用程序中自动完成此操作。
  • 如果任何其他聚合需要对初始聚合所做的更改做出反应,请通过事件订阅者订阅相关事件。
    • 多个订阅者可以订阅任何单个事件。我们不推荐订阅者依赖事件处理顺序,也不推荐订阅者停止事件的传播。虽然我们在 becklyn/ddd-symfony-bridge 中强制执行这些做法,但我们不对您的实现施加任何限制。
    • 订阅者不应包含任何领域逻辑,而应一般只分发新的命令。

实体、聚合和事件

实体必须实现 EventProvider 接口,并且它们必须为它们状态的所有更改引发领域事件。实体可以使用 EventProviderCapabilities 来帮助实现这一点。每个实体还必须有相应的身份类实现 EntityId 接口。AbstractEntityId 提供了一个默认实现。

每个聚合中的一个实体作为聚合根。任何与聚合的交互都只能通过此实体进行,因此只有聚合根应有存储库。聚合根的身份必须实现 AggregateId 接口而不是仅实现 EntityIdAbstractAggregateId 提供了一个默认实现。当在聚合根上调用 dequeueEvents 时,它应该收集聚合中其他实体的所有事件,并返回这些事件以及聚合根引发的事件。

事件必须实现 DomainEvent 接口,并可以通过扩展 AbstractDomainEvent 类来实现。领域事件记录状态更改,并必须包含所有必要数据,以便可以从先前的状态重新播放。

如果使用事件源,聚合应使用 EventSourcedProviderCapabilities 特性而不是 EventProviderCapabilities。虽然可以直接使用 EventStore 和其 getAggregateStream 方法实现存储库,但如果一次获取大量聚合,这可能会对大多数实现造成低性能。对于此类场景,我们建议使用投影。

测试

我们根据 BDD 工作流程的 "给定/当/然后" 编写我们的 PHPUnit/Prophecy 单元测试。为了测试与该库组件交互的代码,我们将各种给定/当/然后辅助方法收集到特质中。您可以在库中各个子域的测试命名空间中找到它们,例如

  • Becklyn\Ddd\Commands\Testing\CommandHandlerTestTrait
  • Becklyn\Ddd\Events\Testing\DomainEventTestTrait