macintoshplus/lite-cqrs

此包已被废弃且不再维护。作者建议使用 broadway/broadway 包。

简单的命令-查询-责任分离库。

v1.1-alpha2 2013-03-02 07:07 UTC

This package is auto-updated.

Last update: 2019-02-20 19:54:22 UTC


README

基于简单命名约定的PHP CQRS库(基于C#的LiteCQRS),它依赖于消息总线、命令、事件和领域事件模式。

Build Status (1.1)

术语

CQS是命令-查询分离:一个范式,其中读取方法永远不会更改状态,写入方法永远不会返回数据。在之上构建,CQRS建议将读取模型与写入模型分离,并使用领域事件模式来通知读取模型关于写入模型的变化。

LiteCQRS使用命令模式和中央消息总线服务来找到执行命令的相应处理器。命令只是一个具有一些属性的类,它可以选择实现LiteCQRS\Command

在执行命令期间,可以触发领域事件。这些事件同样只是具有一些属性的简单类,它们可以选择实现LiteCQRS\DomainEvent

事件队列知道在执行命令期间触发了哪些领域事件,然后将它们发布到事件消息总线,许多监听器可以监听它们。

变更

从1.0到1.1

  • 扩展LiteCQRS\CommandLiteCQRS\DomainEvent不再是必需的。实际上,您可以使用任何类作为命令或事件。仅命名约定本身就可以确保检测命令处理器和事件监听器。

  • JMS序列化插件无法再“分离”事件中作为事件一部分的聚合根属性。因此,将相关的聚合根放入事件中不再受支持(即使使用JMS序列化器0.9也不是一个好的主意)。

约定

  • 命令处理器类的所有公共方法都映射到“命令类简称”=>“方法名”,当方法和命令类简称匹配时。实现命令接口不再是必需的(自1.1起)。
  • 领域事件应用于事件处理器“事件类简称”=>“onEventClassShortname”。只有当此匹配时,才注册事件监听器。
  • 领域事件应用于实体/聚合根“事件类简称”=>“applyEventClassShortname”。
  • 您可以选择扩展具有将数组输入映射到属性并在传递未知属性时抛出异常的构造函数的DefaultDomainEvent
  • 还有一个与DefaultDomainEvent具有相同语义的DefaultCommand。扩展此不是必需的。

示例

  • HelloWorld\GreetingCommand映射到注册处理器的greeting(GreetingCommand $command)方法。
  • HelloWorld\Commands\Greeting映射到注册处理器的greeting(Greeting $command)方法。
  • HelloWorld\GreetedEvent传递给所有具有方法onGreeted(GreetedEvent $event)的事件处理器。
  • HelloWorld\Events\Greeted传递给所有具有方法onGreeted(Greeted $event)的事件处理器。
  • HelloWorld\GreetedEvent 在聚合根创建时委托给 applyGreeted($event)

安装与需求

核心库不依赖于其他库。插件依赖于它们特定的库。

使用 Composer 进行安装

{
    "require": {
        "beberlei/lite-cqrs": "dev-master"
    }
}

工作流程

以下是命令在执行过程中通过 LiteCQRS 栈执行的步骤

  1. 您将命令推送到 CommandBus。命令是您创建的扩展 Command 的简单对象。
  2. CommandBus 检查是否存在可以执行您的命令的处理程序。每个命令恰好有一个处理程序。
  3. 命令处理程序改变领域模型的状态。它通过创建事件(代表状态变化)并将它们传递到您的领域对象的 AggregateRoot::apply()DomainEventProvider::raise() 方法来实现。
  4. 当命令完成时,命令总线将检查身份映射中的所有对象以查找事件。
  5. 所有找到的事件都将传递给 EventMessageBus#publish() 方法。
  6. EventMessageBus 将所有事件分发给观察事件处理程序。
  7. 事件处理程序可以使用 CommandBus 再次创建新的命令。

命令和事件处理程序的执行可以包装在管理事务的处理程序中。事件处理始终在命令事务之外触发。如果命令因任何异常而失败,则命令创建的所有事件都将被遗忘/忽略。在这种情况下不会触发任何事件处理程序。

在 InMemory CommandBus 和 EventMessageBus 的情况下,LiteCQRS 确保命令和事件处理程序的执行永远不会嵌套,而是按照顺序线性化。这防止了每个命令的独立事务相互影响。

示例

请参阅 examples/ 中的示例

  1. example1.php 显示了一个领域对象的命令和事件消息总线使用情况
  2. example2_event.php 显示在命令内部直接使用事件消息总线
  3. example3_sequential_commands.php 展示了命令是如何顺序处理的
  4. tictactoe.php 使用 CQRS 实现了一个井字棋游戏
  5. SymfonyExample.md 显示了在 Symfony2 项目范围内实现 example1.php

设置

  1. 内存命令处理程序,无事件发布/观察
<?php
$userService = new UserService();

$commandBus = new DirectCommandBus()
$commandBus->register('MyApp\ChangeEmailCommand', $userService);
  1. 内存命令和事件处理程序

这使用 LiteCQRS\EventProviderInterface 实例来触发领域事件。

<?php
// 1. Setup the Library with InMemory Handlers
$messageBus = new InMemoryEventMessageBus();
$identityMap = new SimpleIdentityMap();
$queue = new EventProviderQueue($identityMap);
$commandBus = new DirectCommandBus(array(
    new EventMessageHandlerFactory($messageBus, $queue)
));

// 2. Register a command service and an event handler
$userService = new UserService($identityMap);
$commandBus->register('MyApp\ChangeEmailCommand', $userService);

$someEventHandler = new MyEventHandler();
$messageBus->register($someEventHandler);
  1. 内存命令 + 自定义事件队列

LiteCQRS 通过询问 LiteCQRS\Bus\EventQueue 来了解触发的事件。提供您自己的实现,以使您的领域对象无需实现 EventProviderInterface 而独立。

<?php
$messageBus = new InMemoryEventMessageBus();
$queue = new MyCustomEventQueue();

$commandBus = new DirectCommandBus(array(
    new EventMessageHandlerFactory($messageBus, $queue)
));

使用方法

实现您的应用程序的一个用例

  1. 创建一个命令对象,该对象接收所有必要的输入值。使用公共属性并扩展 LiteCQRS\DefaultCommand 以简化。
  2. 在您的任何服务(命令处理程序)中添加一个具有命令名称的新方法
  3. 将命令处理程序注册到 CommandBus 以处理给定的命令。
  4. 让您的实体实现 LiteCQRS\AggregateRootLiteCQRS\DomainEventProvider
  5. 使用受保护的方法 raise(DomainEvent $event)apply(DomainEvent $event) 将事件附加到聚合根对象。

这就是简单用例的全部内容。

如果你的命令触发了监听器检查的事件,你应该

  1. 创建一个特定领域的的事件类。使用公共属性来简化。
  2. 创建事件处理程序或向现有的事件处理程序中添加方法。

虽然为每个用例创建命令和事件似乎“很复杂”。这些对象实际上很简单,只包含公共属性。使用你的IDE或编辑器功能,你可以在短时间内轻松生成它们。反过来,这将使你的代码非常明确。

apply() 和 raise() 的区别

有两种方法可以将事件发布到外部世界。

  • DomainEventProvider#raise(DomainEvent $event) 是简单的,它发出一个事件并做不了更多。
  • AggregateRoot#apply(DomainEvent $event) 要求你添加一个方法 apply$eventName($event),该方法可以用于在对象上重放事件。这是从事件中重放对象的一种方式。

如果你不使用事件源,那么你只需使用 raise() 并完全忽略 apply() 即可。

从 IdentityMap 自动发布事件

你必须实现一个机制来填充 IdentityMapInterface。在这个身份映射中的所有聚合根对象将存储和通过 EventMessageBus 发布其事件。其他所有事件都将被遗忘!

示例:Doctrine ORM 插件实现了 `IdentityMapInterface`。

命令/事件处理程序代理

如果你想将命令/事件处理包裹在自定义逻辑中,你必须扩展 MessageHandlerInterface 并将代理工厂闭包/可调用对象传递给 MessageHandlerInterface

如果你想要记录所有命令

<?php
use LiteCQRS\Bus\MessageHandlerInterface;

class CommandLogger implements MessageHandlerInterface
{
    private $next;

    public function __construct(MessageHandlerInterface $next)
    {
        $this->next = $next;
    }

    public function handle($command)
    {
        syslog(LOG_INFO, "Executing: " . get_class($command));
        $this->next->handle($command);
    }
}

并注册

<?php
$loggerProxyFactory = function($handler) {
    return new CommandLogger($handler);
};
$commandBus = new DirectCommandBus(array($loggerProxyFactory));

失败的事件

EventMessageBus 阻止异常向上传递。为了允许一些失败的事件处理程序执行调试,有一个特殊的事件 "EventExecutionFailed",你可以监听它。你会得到一个 LiteCQRS\Bus\EventExecutionFailed 实例,具有属性 $exception$service$event 以允许分析应用程序中的失败。

扩展点

你应该实现自己的 CommandBus 或扩展现有的,以便将整个过程连接起来,正如你需要它那样工作。

插件

Doctrine

Doctrine 插件附带用于命令和事件的交易包装处理程序。

  • LiteCQRS\Plugin\Doctrine\MessageHandler\DbalTransactionalHandler
  • LiteCQRS\Plugin\Doctrine\MessageHandler\OrmTransactionalHandler

此外,要同步事件到事件消息总线,可以使用 DoctrineIdentityMap

  • LiteCQRS\Plugin\Doctrine\DoctrineIdentityMap

它还附带了一个包装 EntityManager 的 DomainEventProviderRepositoryInterface 实现

  • LiteCQRS\Plugin\Doctrine\ORMRepository

Silex

Silex 插件附带一个 CommandBus 和一个 EventMessageBus,该总线知道如何从你的 Silex 应用程序中获取服务以及一个 ServiceProvider。ServiceProvider 添加了运行 LiteCQRS 所需的最基本服务。

要启用服务提供商,在应用程序中注册它

<?php
$app->register(new LiteCQRS\Plugin\Silex\Provider\LiteCQRSServiceProvider());

lite_cqrs.commands 会自动注入到 ApplicationCommandBus 中。所以要向总线添加命令,扩展服务

<?php

$app['lite_cqrs.commands'] = array_merge($app['lite_cqrs.commands'], array(
    'MyCustom\\SearchCommand' => 'search_handler',
));

请记住,键必须是 Command 类,而值必须是实现了正确处理方法的服务的服务 ID。

要为特定事件添加 EventHandler,需要在 lite_cqrs.event_bus 服务上调用 registerServices

传递给 registerServices 的数组必须看起来像这样

<?php

$eventServices = array(
    'EventName' => 'service_id_id', // or
    'AnotherEvent => array(
        'service_id_1',
        'service_id_2',
    ),
);

Symfony

在 symfony 中,您可以通过使用 lite_cqrs.command_handlerlite_cqrs.event_handler 标签注册服务来使用 LiteCQRS。这些服务随后会自动发现命令和事件。您还可以为标签添加代理消息处理程序工厂。对于命令和事件,标签分别是 lite_cqrs.event_proxy_factorylite_cqrs.command_proxy_factory

CommandBusEventMessageBus 的 Container Aware 实现实现了命令和事件处理器的懒加载,以获得更好的性能。

要启用该捆绑包,请将以下内容放入您的 Kernel 中

new \LiteCQRS\Plugin\SymfonyBundle\LiteCQRSBundle(),

您可以通过添加以下内容来启用/禁用不同的插件到您的 config.yml

lite_cqrs:
    orm:                    true
    swift_mailer:           true
    monolog:                true
    jms_serializer:         true
    crud:                   true
    dbal_event_store:       true
    couchdb_event_store:    true
    couchdb_odm:            true

请参阅 SymfonyExample.md 文档,以了解在 Symfony2 项目中使用 LiteCQRS 的完整演示。

Swiftmailer

Swiftmailer 插件允许您在命令或事件处理程序成功完成后延迟发送邮件。

  • LiteCQRS\Plugin\Swiftmailer\SpoolTransportHandler

您需要一个 spool 传输和一个真实传输实例。Spool 传输将所有消息排队,如果命令/事件处理程序成功执行,则通过真实传输发送所有消息。

Monolog

一个插件,它使用 Monolog 记录每个命令和处理器。它包括消息的类型和名称,其参数作为 json,以及其执行是否成功。

Monolog 在 Symfony 中的集成注册了一个特定的通道 lite_cqrs,您可以将其配置为不同于 Symfony 中的默认通道。有关更多信息,请参阅 Symfony cookbook

JMS Serializer

警告:此插件不与 JMS Serializer 0.10 及以上版本兼容,请在 Symfony 中将其禁用 serializer: false

一个插件,它使用 JMS Serializer 将事件序列化为 JSON。这对于高级事件日志记录是必要的。它使用自定义类型处理程序将事件中的聚合根对象转换为引用,并在重建时再次获取它们。这样,您就不会将数据图序列化到事件存储中。

Doctrine CouchDB

一个包含 Doctrine CouchDB EventStore 和事务处理程序的插件的插件。

CRUD

虽然 CRUD 和 CQRS 通常不匹配,但如果您在写入模型中使用 Doctrine 作为主要数据源,则可以使用 PHP 的动态功能,结合 LiteCQRS 和此插件来实现 CRUD。

使用 AggregateResource 抽象类或 CrudCreatableCrudUpdatableCrudDeletable 特性,您可以实现 CRUD 功能。这可以通过三个命令实现

  • LiteCQRS\Plugin\CRUD\Model\Commands\CreateResourceCommand
  • LiteCQRS\Plugin\CRUD\Model\Commands\UpdateResourceCommand
  • LiteCQRS\Plugin\CRUD\Model\Commands\DeleteResourceCommand

它们具有 $class$id$data 属性。在创建和更新命令中,使用批量赋值将 $data 应用到模型。您必须确保这是对您的模型安全操作,通过自己实现 apply*() 方法来代替依赖于批量赋值。

在处理以下三个领域事件之一后,会发出

  • LiteCQRS\Plugin\CRUD\Model\Events\ResourceCreatedEvent
  • LiteCQRS\Plugin\CRUD\Model\Events\ResourceUpdatedEvent
  • LiteCQRS\Plugin\CRUD\Model\Events\ResourceDeletedEvent