flow2lab / eventsourcing
用于使用EventSourcing的包
Requires
- dbellettini/eventstore-client: ~0.4
- pda/pheanstalk: 3.0.*
- typo3/flow: dev-master
This package is not auto-updated.
Last update: 2024-09-20 19:09:32 UTC
README
本包为TYPO3 Flow提供基本的CQRS/ES基础设施。
其目的是提供灵感,以便编写自己的定制化工具集,用于处理CQRS/ES。 我不建议在不理解底层概念之前使用此包。
安装
$ composer require flow2lab/eventsourcing dev-master
命令
命令定义了与领域模型的接口。它们进入命令总线,然后将被(同步/异步)由命令处理器处理。命令从不返回任何数据,因此处理命令的接口类型为void。
定义命令
您可以通过从Flow2Lab\EventSourcing\Command\Command扩展来定义一个命令(我将很快更新代码库以使用CommandInterface,但在那之前您需要从Command继承)。命令有一个commandId,可以用于日志记录目的,如果需要的话。
<?php namespace Vendor\Foo\Command; use Flow2Lab\EventSourcing\Command\Command; /** * Any doc comment will be part of the CLI documentation! */ class AddInventoryItemToBasket extends Command { /** * @var string */ public $basketId; /** * You will see this in the CLI! * * @var string */ public $inventoryItemId; /** * @param string $basketId * @param string $inventoryItemId */ public function __construct($basketId, $inventoryItemId) { parent::__construct(); $this->basketId = $basketId; $this->inventoryItemId = $inventoryItemId; } }
命令处理器
命令处理器执行来自命令总线的命令。它们是应用程序层的一部分,并且仅协调领域模型。它们的任务是解决依赖关系并将它们传递给领域模型。
您可以通过实现接口Flow2Lab\EventSourcing\Command\Handler\CommandHandlerInterface来定义一个命令处理器。或者,您可以扩展Flow2Lab\EventSourcing\Command\CommandHandler,这将提供正常处理器的默认实现。
flow2lab.EventSourcing提供的命令处理器具有以下命名约定:handle<CommandName>Command。命令名称是简单的类名。为了避免冲突,还需要确保命令处理器的命名空间为CommandHandler,命令的命名空间为Command。请参见下面的示例文件夹结构和实现。
- Vendor
- Foo
- CommandHandler
- BasketCommandHandler.php
- Command
- AddInventoryItemToBasket.php
以下是BasketCommandHandler实现,处理AddInventoryItemToBasket命令。
<?php namespace Vendor\Foo\CommandHandler; use Vendor\Foo\Command\AddInventoryItemToBasket; use Vendor\Foo\Domain\Repository\BasketRepository; use Flow2Lab\EventSourcing\Command\CommandHandler; use TYPO3\Flow\Annotations as Flow; /** * @Flow\Scope("singleton") */ class BasketCommandHandler extends CommandHandler { /** * @var BasketRepository * @Flow\Inject */ protected $basketRepository; /** * @param AddInventoryItemToBasket $command */ protected function handleAddInventoryItemToBasketCommand(AddInventoryItemToBasket $command) { // let's assume it exists $basket = $this->basketRepository->find($command->basketId); $basket->addInventoryItem($command->inventoryItemId); $this->basketRepository->save($basket); } }
命令总线
为了使命令由其命令处理器处理,使用命令总线。您可以在事件处理器或ActionControllers中注入命令总线。以下示例说明了在ActionController中的使用。
<?php namespace Vendor\Foo\Controller; // ... NS imports class BasketController extends ActionController { /** * @var InternalCommandBus * @Flow\Inject */ protected $commandBus; /** * @param string $basketId * @param string $inventoryItemId */ public function addInventoryItemToBasketAction($basketId, $inventoryItemId) { // exceptions can and will be thrown here, catch accordingly // at least, until error handling is implemented $this->commandBus->handle(new AddInventoryItemToBasket( $basketId, $inventoryItemId )); // redirect } }
理想情况下,您将编写一个TypeConverter来自动将例如POST数据转换为命令,并在单个ActionController中处理它们。
使用CLI
默认情况下,所有命令都通过Flow CLI公开。一旦定义,您可以使用
$ ./flow help
$ ./flow help foo:addinventoryitemtobasket
$ ./flow foo:addinventoryitemtobasket --basket-id="2" --inventory-item-id="1"
来查看任何给定命令所需的参数并执行它们。
要禁用命令CLI访问,编辑项目的Settings.yaml,如下所示
flow2lab: EventSourcing: Command: Controller: enabled: false
或者,您可以通过将其标记为内部命令来隐藏命令供普通CLI用户使用。这样,命令仍然可以访问,但不再由./flow help打印。
flow2lab: EventSourcing: Command: Controller: enabled: true markAsInternal: true
事件
领域事件由聚合体创建,通过存储库检索,存储在事件存储中,并通过事件总线发布。它们是事件源模型中的唯一真相来源,并定义了您的模型状态。
定义事件
事件通过扩展Flow2Lab\EventSourcing\Event\DomainEvent来定义。请注意,必须调用父类的构造函数以生成事件发生时的日期(我仍在考虑是否通过例如AOP来完成此操作)。
<?php namespace Vendor\Foo\Domain\Event; use Flow2Lab\EventSourcing\Event\DomainEvent; class InventoryItemToBaskedAdded extends DomainEvent { /** * @var string */ public $basketId; /** * @var string */ public $inventoryItemId; /** * @param string $basketId * @param string $inventoryItemId */ public function __construct($basketId, $inventoryItemId) { parent::__construct(); $this->basketId = $basketId; $this->inventoryItemId = $inventoryItemId; } }
存储库
存储库用于保存和检索聚合体。Flow的命名约定也适用于此处。对于聚合体Basket的BasketRepository看起来是这样的
namespace Vendor\Foo\Domain\Repository; use Flow2Lab\EventSourcing\Store\Repository; use TYPO3\Flow\Annotations as Flow; /** * @Flow\Scope("singleton") */ class BasketRepository extends Repository {}
默认的事件存储后端是EventStore的EventStoreBackend。您可以实现StoreBackendInterface并使用Objects.yaml来使用自己的实现。
事件总线
通常只有存储库应使用事件总线。我很快会添加一些接口,允许根据事件是否需要异步或同步处理来配置不同的队列。
事件处理器
与命令处理器类似,事件处理器处理在事件总线上发布的事件。事件处理器订阅一个或多个领域事件。要创建事件处理器并使其监听事件,实现EventHandlerInterface(异步)或ImmediateEventHandlerInterface(同步)接口。然后,根据接口,事件总线将事件推送到队列或直接传递它们进行处理(将通过不同的队列完成,但API不应更改)。
通常您会使用事件处理器来处理最终一致性或发送电子邮件等。以下示例说明了AbstractEventHandler的用法及其约定。
定义事件处理器
<?php namespace Vendor\Foo\Service; // ... NS imports class InformingCustomerAboutAddedInventoryItemService extends AbstractEventHandler implements ImmediateEventHandlerInterface { /** * @var array */ protected $subscribedToEvents = [ InventoryItemToBaskedAdded::class ]; /** * @param InventoryItemToBaskedAdded $event */ protected function handleInventoryItemToBaskedAddedEvent(InventoryItemToBaskedAdded $event) { // this is probably a bad idea to do, but certainly possible // needs some more checking though ;) $this->flashMessageContainer->addFlashMessage('Hi! The inventory item "' . $event->inventoryItemId . '" has been added to your basket.'); } }
聚合体
在事件源环境中,仅使用领域事件重建聚合体!您可以通过实现Flow2Lab\EventSourcing\AggregateRootInterface来创建聚合类。特性Flow2Lab\EventSourcing\AggregateSourcing包含满足接口的行为,并为您的聚合体提供合理的默认实现。
实例化
您可以使用类似于其他PHP类的方式实例化新的聚合实例。请注意,没有自动标识符生成。特性AggregateSourcing添加了一个属性$identifier,但不会填充它。当考虑命令时,您很可能希望在发送命令之前生成聚合体的标识符。
$basket = new Basket('123');
发布新的领域事件
一旦创建,您就可以发布领域事件。领域事件仅从聚合体(或实体)内部发布。
public function addInventoryItem($inventoryItemId) { // business logic // validation logic $this->applyNewEvent(new InventoryItemToBaskedAdded($this->identifier, $inventoryItemId)); }
现在,当在存储库中保存此聚合体时,新的事件将写入事件存储。
从事件流中加载聚合体
通常情况下,存储库会为您处理现有聚合的加载。然而,在测试时,您可能需要手动加载它们。特性行为AggregateSourcing实现了静态方法loadFromEventStream,该方法将实例化和应用事件。
$existingBasket = Basket::loadFromEventStream([ new BasketCreated(123), new InventoryItemToBaskedAdded(123, 1) ]);
然后,特性行为将不调用其构造函数而实例化一个新的Basket实例,然后应用每个事件。为此,您必须实现事件应用方法。这种方法的约定是on<EventName>。
/** * @param BasketCreated $event */ protected function onBasketCreated(BasketCreated $event) { $this->identifier = $event->basketId; } /** * @param InventoryItemToBaskedAdded $event */ protected function onInventoryItemToBaskedAdded(InventoryItemToBaskedAdded $event) { $this->inventoryItems[] = $event->inventoryItemId; }
如果由于缺少应用方法而无法应用事件,特性行为将抛出异常。在处理模型仅有的CRUD部分时,您可能希望对此类事件的处理不那么严格,在这些部分中,您没有与某些属性关联的业务逻辑。这样,您可以在模型中获得精炼的业务逻辑,而不带任何噪声。
购物篮聚合
<?php namespace Vendor\Foo\Domain\Model; // ... NS imports class Basket implements AggregateRootInterface { use AggregateSourcing; /** * @var array<string> */ protected $inventoryItems = []; /** * @param string $basketId */ public function __construct($basketId) { $this->applyNewEvent(new BasketCreated($basketId)); } /** * @param string $inventoryItemId */ public function addInventoryItem($inventoryItemId) { // business logic // validation logic $this->applyNewEvent(new InventoryItemToBaskedAdded($this->identifier, $inventoryItemId)); } /** * @param BasketCreated $event */ protected function onBasketCreated(BasketCreated $event) { $this->identifier = $event->basketId; } /** * @param InventoryItemToBaskedAdded $event */ protected function onInventoryItemToBaskedAdded(InventoryItemToBaskedAdded $event) { $this->inventoryItems[] = $event->inventoryItemId; } }
实体
实体在技术上与聚合非常相似,但是它们由聚合维护和拥有。实体的生命周期是聚合的责任,并且您只能通过聚合访问实体。
实体必须实现Flow2Lab\EventSourcing\EntityInterface,特性行为Flow2Lab\EventSourcing\EntitySourcing提供行为。
在创建实体时,您必须将实体注册到聚合中
public function onSomeThingsHappened(SomeThingsHappened $event) { $entity = new Entity(..); $this->entities[$entity->getIdentifier()] = $entity; $this->registerEntity($entity); }
请注意,聚合正在处理创建实体的事件,而不是实体。这就是为什么在Entity::__construct方法中没有检查的原因,因为这个方法类似于应用事件的函数。
class Entity implements EntityInterface { use EntitySourcing; public function __construct($entityId) { $this->identifier = $entityId; } }
这确保事件也转发到每个实体。然后,实体必须通过实现canApplyEvent来公开它们订阅的事件。
public function canApplyEvent(DomainEvent $event) { if ($event instanceof EntityEvent) { return ($event->entityId === $this->identifier); } return FALSE; }
事件应用的工作方式与在聚合根中重新播放事件完全相同。
存储
目前,实现的唯一存储后端是EventStore。您可以实现StoreBackendInterface,并使用Objects.yaml来使用自己的实现。
投影
投影,也称为查询模型,用于在事件源环境中查询数据。基本上,它们只是事件处理器,用于更新某些查询优化的数据库。
待办事项:为MysqlProjector添加一些示例
队列
消息队列可用于异步或同步地处理命令和事件。目前,唯一有效的队列是处理消息的同步队列ImmediateQueue。我的计划是在将来使用TYPO3.Jobqueue(无需重新发明轮子)。
序列化
目前,实现了两个序列化器
- ArraySerializer:将消息转换为数组
- JsonSerializer:将消息转换为JSON字符串
测试
待办事项:关于事件源模型如何测试的说明(提示:不是使用getter;)
待办事项
- 完成此文档
- 在所有处理程序(命令/事件/投影)中处理异常
- 实现快照
- 为此包编写测试(是的,这完全未经过测试,但它工作:o!)
许可证
Flow2Lab.EventSourcing是在MIT许可证下发布的。