becklyn / ddd-core
PHP的DDD框架
Requires
- php: >=8.0
- becklyn/utilities: ^2.0
- illuminate/collections: ^8.12 || ^9.0
- ramsey/uuid: ^4.0
- webmozart/assert: ^1.9
Requires (Dev)
- phpspec/prophecy-phpunit: ^2.0
- phpunit/phpunit: ^9.1
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
接口的聚合(我们建议从存储库中加载)。 - 在聚合上执行操作,在其中引发领域事件(您可以使用聚合中的
EventProviderCapabilities
或EventSourcedProviderCapabilities
特性来简化此操作)。 - 从
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
接口而不是仅实现 EntityId
。AbstractAggregateId
提供了一个默认实现。当在聚合根上调用 dequeueEvents
时,它应该收集聚合中其他实体的所有事件,并返回这些事件以及聚合根引发的事件。
事件必须实现 DomainEvent
接口,并可以通过扩展 AbstractDomainEvent
类来实现。领域事件记录状态更改,并必须包含所有必要数据,以便可以从先前的状态重新播放。
如果使用事件源,聚合应使用 EventSourcedProviderCapabilities
特性而不是 EventProviderCapabilities
。虽然可以直接使用 EventStore
和其 getAggregateStream
方法实现存储库,但如果一次获取大量聚合,这可能会对大多数实现造成低性能。对于此类场景,我们建议使用投影。
测试
我们根据 BDD 工作流程的 "给定/当/然后" 编写我们的 PHPUnit/Prophecy 单元测试。为了测试与该库组件交互的代码,我们将各种给定/当/然后辅助方法收集到特质中。您可以在库中各个子域的测试命名空间中找到它们,例如
Becklyn\Ddd\Commands\Testing\CommandHandlerTestTrait
Becklyn\Ddd\Events\Testing\DomainEventTestTrait