headsnet / domain-events-bundle
将域事件集成到您的 Symfony 应用程序中
Requires
- php: ^7.4 || ^8.0.2 || ^8.1
- doctrine/doctrine-bundle: ^1.0 || ^2.0
- doctrine/orm: ^2.5
- ramsey/uuid-doctrine: ^1.5
- symfony/event-dispatcher: ^4.4 || ^5.0 || ^6.0 || ^7.0
- symfony/framework-bundle: ^4.4 || ^5.0 || ^6.0 || ^7.0
- symfony/lock: ^4.4 || ^5.0 || ^6.0 || ^7.0
- symfony/messenger: ^4.4 || ^5.0 || ^6.0 || ^7.0
- symfony/property-access: ^4.4 || ^5.0 || ^6.0 || ^7.0
- symfony/serializer: ^4.4 || ^5.0 || ^6.0 || ^7.0
Requires (Dev)
- nyholm/symfony-bundle-test: ^2.0 || ^3.0
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^9.5
- symplify/easy-coding-standard: ^11.1
Suggests
- ext-amqp: *
README
DDD 域事件用于 Symfony,基于 Doctrine 的事件存储。
此包允许您从域模型内部发布域事件,以便它们与聚合事务一起持久化。
然后,使用 Symfony 事件监听器在 kernel.TERMINATE
事件中发布这些事件。
这确保了事务一致性和通过 Outbox 模式保证的投递。
需要 Symfony 4.4 或更高版本
安装
composer require headsnet/domain-events-bundle
(有关先决条件,请参阅下面的 Messenger 组件)
域事件类
域事件类必须使用聚合根 ID 实例化。
您可以根据需要向构造函数添加其他参数。
use Headsnet\DomainEventsBundle\Domain\Model\DomainEvent; use Headsnet\DomainEventsBundle\Domain\Model\Traits\DomainEventTrait; final class DiscountWasApplied implements DomainEvent { use DomainEventTrait; public function __construct(string $aggregateRootId) { $this->aggregateRootId = $aggregateRootId; $this->occurredOn = (new \DateTimeImmutable)->format(DateTime::ATOM); } }
记录事件
域事件应从您的域模型内部发布 - 即直接从您的实体内部。
在这里,我们记录了一个实体创建的域事件。然后,它将自动与主实体持久化相同的数据库事务一起持久化到 Doctrine 的 event
数据库表。
use Headsnet\DomainEventsBundle\Domain\Model\ContainsEvents; use Headsnet\DomainEventsBundle\Domain\Model\RecordsEvents; use Headsnet\DomainEventsBundle\Domain\Model\Traits\EventRecorderTrait; class MyEntity implements ContainsEvents, RecordsEvents { use EventRecorderTrait; public function __construct(PropertyId $uuid) { $this->uuid = $uuid; // Record a domain event $this->record( new DiscountWasApplied($uuid->asString()) ); } }
然后,在 kernel.TERMINATE
事件中,一个监听器自动将域事件发布到 messenger.bus.event
事件总线以供其他地方消费。
修改域事件
尽管事件应该被视为不可变的,但在将它们添加到事件存储之前添加或更改元数据可能很方便。
在将域事件附加到事件存储之前,标准的 Doctrine 事件存储会发出一个 PreAppendEvent
Symfony 事件,例如,可以用来设置操作者 ID,如下面的示例所示
use App\Entity\User; use Headsnet\DomainEventsBundle\Doctrine\Event\PreAppendEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Security; final class AssignDomainEventUser implements EventSubscriberInterface { private Security $security; public function __construct(Security $security) { $this->security = $security; } public static function getSubscribedEvents(): array { return [ PreAppendEvent::class => 'onPreAppend' ]; } public function onPreAppend(PreAppendEvent $event): void { $domainEvent = $event->getDomainEvent(); if (null === $domainEvent->getActorId()) { $user = $this->security->getUser(); if ($user instanceof User) { $domainEvent->setActorId($user->getId()); } } } }
将事件延迟到未来
如果您为 DomainEvent::occurredOn
指定一个未来日期,则事件将在此日期之前不发布。
这允许直接从域模型中安排任务。
可替换的未来事件
如果事件实现了 ReplaceableDomainEvent
而不是 DomainEvent
,则对于相同的聚合根记录同一事件的多个实例将覆盖先前的事件记录,前提是它尚未发布。
例如,假设您有一个聚合 Booking,它有一个未来的 ReminderDue 事件。如果预订随后被修改为具有不同的日期/时间,则提醒也必须进行修改。通过实现 ReplaceableDomainEvent
,您可以简单地记录一个新的 ReminderDue 事件,并且只要先前的 ReminderDue 事件尚未发布,它将被删除并替换为新的 ReminderDue 事件。
事件发布
默认情况下,只有 DomainEvent 被发布到配置的事件总线。
您可以通过用自己的实现覆盖默认事件分发器,在发布之前对消息进行注解,例如添加带有自定义戳的封装。
示例
services: headsnet_domain_events.domain_event_dispatcher_service: class: App\Infrastructure\DomainEventDispatcher
class PersonCreated implements DomainEvent, AuditableEvent { … }
class DomainEventDispatcher implements \Headsnet\DomainEventsBundle\EventSubscriber\DomainEventDispatcher { private MessageBusInterface $eventBus; public function __construct(MessageBusInterface $eventBus) { $this->eventBus = $eventBus; } public function dispatch(DomainEvent $event): void { if ($event instanceof AuditableEvent) { $this->eventBus->dispatch( new Envelope($event, [new AuditStamp()]) ); } else { $this->eventBus->dispatch($event); } } }
Messenger 组件
默认情况下,该包期望存在一个名为 messenger.bus.event
的消息总线。这可以通过包配置进行配置 - 请参阅 默认配置。
framework: messenger: … buses: messenger.bus.event: # Optional default_middleware: allow_no_handlers
Doctrine
该包将在发布之前创建一个名为 event
的数据库表来持久化事件。这允许记录所有引发的事件的永久记录。
数据库表名可以进行配置 - 请参阅下面的默认配置。
StoredEvent
实体还跟踪每个事件是否已发布到总线。
最后,一个名为datetime_immutable_microseconds
的Doctrine DBAL自定义类型会自动注册。这允许StoredEvent实体以微秒精度持久化事件。这确保事件以与记录时完全相同的顺序发布。
旧版事件类
在重构过程中,您可能会移动或重命名事件类。这将导致数据库中存储旧类名。
有一个控制台命令,将报告这些不匹配现有代码库中当前类的旧事件类(基于Composer自动加载)。
bin/console headsnet:domain-events:name-check
然后您可以定义legacy_map
配置参数,将旧的事件类名映射到它们的新的替代品。
headsnet_domain_events: legacy_map: App\Namespace\Event\YourLegacyEvent1: App\Namespace\Event\YourNewEvent1 App\Namespace\Event\YourLegacyEvent2: App\Namespace\Event\YourNewEvent2
然后,您可以重新运行控制台命令并使用--fix
选项。这将更新数据库中旧类名的新引用。
还有一个--delete
选项,如果旧映射中找不到旧事件,它将从数据库中删除所有旧事件。这是一个破坏性命令,请谨慎使用。
默认配置
headsnet_domain_events: message_bus: name: messenger.bus.event persistence: table_name: event legacy_map: []
贡献
欢迎贡献。请为每个修复/功能提交一个拉取请求。
Composer脚本已为您配置好,以便方便使用。
> composer test # Run test suite
> composer cs # Run coding standards checks
> composer cs-fix # Fix coding standards violations
> composer static # Run static analysis with Phpstan