flow2lab/eventsourcing

用于使用EventSourcing的包

安装: 0

依赖项: 0

建议者: 0

安全: 0

星标: 0

关注者: 4

分支: 3

公开问题: 4

类型:typo3-flow-package

dev-master 2016-04-04 07:30 UTC

This package is not auto-updated.

Last update: 2024-09-20 19:09:32 UTC


README

Code Climate Test Coverage Issue Count Latest Stable Version Total Downloads Latest Unstable Version License

本包为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的命名约定也适用于此处。对于聚合体BasketBasketRepository看起来是这样的

namespace Vendor\Foo\Domain\Repository;

use Flow2Lab\EventSourcing\Store\Repository;
use TYPO3\Flow\Annotations as Flow;

/**
 * @Flow\Scope("singleton")
 */
class BasketRepository extends Repository {}

默认的事件存储后端是EventStoreEventStoreBackend。您可以实现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许可证下发布的。