PHP的CQRS + Event Sourcing库包:dudulina

18-07-01 banq
                   

PHP号称是最好的服务器端编程语言,CQRS + Event Sourcing已经在PHP社区和.NET社区蔓延开来,而Java社区由于Spring和JavaEE的垄断相对要落后些,废话少说,看看这款PHP库包dudulina的特点:

1.领域模型代码最小化依赖该库包:

只需要实现三个接口,没有extends继承(接口实现高于类继承),这样你的领域代码就会保持干净,对自己运行在哪种基础架构或框架中就会毫不知情。

(1)\Dudulina\Event :每个领域事件实现这个接口; 它没有方法,它只是一个标记接口; 领域事件会被自动代码生成工具检测到;

(2)\Dudulina\Command:每个领域命令会实现这个接口; 它只有一种方法getAggregateId(); 命令调度器需要根据这个ID从Repository加载对应的Aggregate实例

(3)\Dudulina\ReadModel\ReadModelInterface:这是每个读模型需要实现的; 这仅在使用ReadModelRecreator重建读模型(预测)时才是必需的。

通过实现少数几个接口可以更加与dudulina松散耦合。可以定义和使用自己的领域接口,这个接口可以继承dudulina库包。这样,当您更改库时,只需更新这些接口就可以了。

2. 写入端的最小代码重复

在写入方面,您只需要实例化一个命令并将其发送给CommandDispatcher;

我们来创建一个命令:

// immutable and Plain PHP Object (Value Object)
// No inheritance!
class DoSomethingImportantCommand implements Command
{
    private $idOfTheAggregate;
    private $someDataInTheCommand;

    public function __construct($idOfTheAggregate, $someDataInTheCommand)
    {
        $this->idOfTheAggregate = $this->idOfTheAggregate;
        $this->someDataInTheCommand = $this->someDataInTheCommand;
    }

    public function getAggregateId()
    {
        return $this->idOfTheAggregate;
    }

    public function getSomeDataInTheCommand()
    {
        return $this->someDataInTheCommand;
    }
}
<p>

创建一个简单事件:

// immutable, simple object, no inheritance, minimum dependency
class SomethingImportantHappened implements Event
{
    public function __construct($someDataInTheEvent)
    {
        $this->someDataInTheEvent = $someDataInTheEvent;
    }

    public function getSomeDataInTheEvent()
    {
        return $this->someDataInTheEvent;
    }
}
<p>

下面是UI代码:

class SomeHttpAction
{
    public function getDoSomethingImportant(RequestInterface $request)
    {
        $idOfTheAggregate = $request->getParsedBody()['id'];
        $someDataInTheCommand = $request->getParsedBody()['data'];

        $this->commandDispatcher->dispatchCommand(new DoSomethingImportantCommand(
            $idOfTheAggregate,
            $someDataInTheCommand
        ));

        return new JsonResponse([
            'success' => 1,
        ]);
    }
}
<p>

只需要实现命令和事件,以及必要的UI,其他什么也不用做,就是这样,没有事务管理,不需要从存储库加载,什么也没有。命令作为一个参数传入聚合的命令处理器,如下所示:

class OurAggregate
{
    //....
    public function handleDoSomethingImportant(DoSomethingImportantCommand $command)
    {
        if($this->ourStateDoesNotPermitThis()){
            throw new \Exception("No no, it is not possible!");
        }

        yield new SomethingImportantHappened($command->getSomeDataInTheCommand());
    }

    public function applySomethingImportantHappened(SomethingImportantHappened $event, Metadata $metadata)
    {
        //Metadata is optional
        $this->someNewState = $event->someDataInTheEvent;
    }
}
<p>

读取模型将接收到上面命令处理器引发的事件,并在保存事件之后处理事件,看一看可读模型:

class SomeReadModel
{
    //...some database initialization, i.e. a MongoDB database injected in the constructor

    public function onSomethingImportantHappened(SomethingImportantHappened $event, Metadata $metadata)
    {
        $this->database->getCollection('ourReadModel')->insertOne([
            '_id' => $metadata->getAggregateId()
            'someData' => $event->getSomeDataInTheEvent()
        ]);
    }

    //this method could be used by the UI to display the data
    public function getSomeDataById($id)
    {
        $document = $this->database->getCollection('ourReadModel')->findOne([
            '_id' => $metadata->getAggregateId()
         ]);

         return $document ? $document['someData'] : null;
    }
}
<p>

读模型在另外一个单独进程更新专门的读数据库,近似实时(通过tailing),或从事件存储中轮询甚至可以使用Javascript。

当前端命令到达被分派处理时时,会发生以下过程:

1. 根据Id定位到聚合实例

2. 从数据库库加载 聚合,重放所有以前的事件

3. 该命令被分派到对应聚合实例

4. 聚合产生事件

5. 事件写入保存到专门的事件数据库

6. 读模型会收到新的写入事件通知,并更新专门的读数据库

7. Safa也会被通知; 如果saga(事务管理器)产生其他命令,则循环再次开始。

GitHub - xprt64/dudulina: CQRS + Event Sourcing li

                   

3