PHP的CQRS + Event Sourcing库包:dudulina

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;
}
}

创建一个简单事件:


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

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

下面是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,
]);
}
}

只需要实现命令和事件,以及必要的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;
}
}

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


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;
}
}

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

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

1. 根据Id定位到聚合实例
2. 从数据库库加载 聚合,重放所有以前的事件
3. 该命令被分派到对应聚合实例
4. 聚合产生事件
5. 事件写入保存到专门的事件数据库
6. 读模型会收到新的写入事件通知,并更新专门的读数据库
7. Safa也会被通知; 如果saga(事务管理器)产生其他命令,则循环再次开始。


GitHub - xprt64/dudulina: CQRS + Event Sourcing li