xprt64/dudulina

PHP的CQRS与事件源迷你框架


README

MIT Licence Build Status Scrutinizer Code Quality Code Climate Code Coverage Build Status

这是一个非侵入式的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