xprt64 / dudulina
PHP的CQRS与事件源迷你框架
Requires
- php: ^8.0
- psr/container: ^1.0
- psr/log: ^1.0
- xprt64/code-analysis: ^1.1.15
- xprt64/iterator: ^1.0
- xprt64/selector: ^0.0.3
- xprt64/types: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5.10
- dev-master
- 6.0.1
- 6.0
- 5.1.10
- 5.1.9
- 5.1.8
- 5.1.7
- 5.1.6
- 5.1.5
- 5.1.4
- 5.1.3
- 5.1.2
- 5.1.1
- 5.1.0
- 5.0.0
- 4.2.6
- 4.2.5
- 4.2.4
- 4.2.3
- 4.2.2
- 4.2.1
- 4.2.0
- 4.1.13
- 4.1.12
- 4.1.11
- 4.1.10
- 4.1.9
- 4.1.8
- 4.1.7
- 4.1.6
- 4.1.5
- 4.1.4
- 4.1.3
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.10
- 4.0.9
- 4.0.8
- 4.0.7
- 4.0.6
- 4.0.5
- 4.0.4
- 4.0.3
- 4.0.2
- 4.0.1
- 4.0.0
- 3.0.10
- 3.0.9
- 3.0.8
- 3.0.7
- 3.0.6
- 3.0.5
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3
- 2.1.13
- 2.1.12
- 2.1.11
- 2.1.10
- 2.1.9
- 2.1.8
- 2.1.7
- 2.1.6
- 2.1.5
- 2.1.4
- 2.1.3
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.9
- 2.0.8
- 2.0.7
- 2.0.6
- 2.0.5
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 1.2.7
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.9
- 1.1.8
- 1.1.7
- 1.1.6
- 1.1.5
- 1.1.4
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- 0.0.1
This package is auto-updated.
Last update: 2024-09-22 22:24:50 UTC
README
这是一个非侵入式的CQRS + 事件源库,帮助构建复杂的DDD Web应用程序。
在领域代码中对库的最小依赖
只需实现3个接口
没有继承!您的领域代码保持干净,不依赖于基础设施/框架,应该是这样。
-
\Dudulina\Event
为每个领域事件;没有方法,它只是一个标记接口;领域事件需要由自动代码生成工具检测; -
\Dudulina\Command
为每个领域命令;只有一个方法,getAggregateId()
;这是命令派发器需要知道从存储库加载Aggregate实例的原因; -
\Dudulina\ReadModel\ReadModelInterface
为每个只读模型;如果您使用ReadModelRecreator
来重建您的只读模型(投影),则这是必需的;
即使只需要实现几个接口,您也可以进一步减少对库的耦合。您可以定义并使用自己的领域接口,只有那些接口会继承库接口。这样,当您更改库时,您只需更改这些接口。
在写入方面的最小代码重复
在写入方面,您只需实例化一个命令并将其发送到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 = $idOfTheAggregate; $this->someDataInTheCommand = $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, ]); } }
就这样。没有事务管理,没有从存储库加载,什么都没有。命令作为参数到达聚合的命令处理器,如下所示
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; } }
只读模型可以在单独的进程中更新,类似于实时(通过尾部跟踪)或轮询事件存储,甚至使用JavaScript。有关如何保持只读模型更新的更多信息,请参阅此处。
因此,当命令被派发时,以下事情会发生
- 确定聚合类
- 从存储库加载聚合,重放所有以前的事件
- 将命令派发到聚合实例
- 聚合产生事件
- 将事件持久化到事件存储
- 通知只读模型有关新事件
- 也通知Sagas;如果Sagas生成其他命令,循环再次开始。
如果聚合的命令处理器抛出异常,则不会持久化事件,异常会到达调用者
在此处阅读完整的文档
事件存储
有一个MongoDB实现的事件存储,以及一个用于此事件存储的Restful HTTP API,如果您想用其他语言构建只读模型。
还有一个JavaScript连接器。您可以在此处找到更新只读模型的一些JavaScript示例。
查询
该库还可以派发查询。提问者提出问题,而回答者回答它们。
询问者向 \Dudulina\Query\Asker
提出问题,并可以接收返回值或回调(方法 $this->whenAnsweredXYZ
或标记有 @QueryAsker
的方法)作为答案。
回答者在 $this->whenAskedXXX
或标记有 @QueryHandler
的方法中回答问题。当知道答案已更改且所有询问者都已通知时,他们也可以通过调用 \Dudulina\Query\Answerer::answer()
来回答问题。
CQRS 绑定
当分发命令时,库如何知道调用哪个命令处理器?或者当发布新事件时,如何知道通知哪些读取模型?所有这些问题的答案都是 CQRS 绑定。
简而言之,工具 分析领域代码,检测处理器,并构建一个包含所有绑定的 PHP 文件作为类。然后您可以使用这些类来配置 CommandDispatcher。每次领域代码更改时都必须运行 create_bindings.php
。
php -f vendor/xprt64/dudulina/bin/create_bindings.php -- --src="/some/source/directory" --src="/some/other/source/directory" > cqrs_bindings.php
然后,您需要将文件 create_bindings.php
包含到 index.php
中,通常在 vendors/autoload.php
之后。
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../deploy/cqrs_bindings.php';
示例应用
一个待办事项列表的示例应用可以在 github.com/xprt64/todosample-cqrs-es 上找到。
在 DDD 中查询聚合
了解更多关于如何查询聚合以测试命令是否成功,而不实际执行它的信息。
有问题?
请随意在此组中发布: https://groups.google.com/forum/#!forum/cqrs--event-sourcing-for-php。