lidskasila / glow
简单的[C]ommand [Q]uery [R]esponsibility [S]egregation和[E]vent [S]ourcing库。
Requires
- php: >=7.1
- ramsey/uuid: ^3.6
- roave/security-advisories: dev-master
Requires (Dev)
- mockery/mockery: ^0.9
- phing/phing: ^2.16
- phpunit/phpunit: ^6.2
This package is not auto-updated.
Last update: 2024-09-15 01:43:43 UTC
README
这个库旨在展示另一种处理CQRS和事件源的方法,但并不认为它适合用于实际的production项目。为此,更建议使用Prooph工具箱或Nette扩展Prooph工具箱。它支持快照、读取模型投影,易于进行事件回放,并且更加灵活。这个PHP CQRS EventSourcing库基于Benjamin Eberlei的LiteCQRS for php,该库长时间没有维护。这个分支使其重新焕发生机,但它不应被视为稳定的库,因为未来可能会出现BC breaks。
主要区别在于
- PHP的最低要求版本是7.0,很快将升级到7.1。
- 命令 - 只可以向CommandBus注册ComandHandler(只有handle方法)的实现。这样做的原因是为了强制执行命名和结构约定。当你看到XxxCommand并需要看到实现时,你知道应该在某个地方实现XxxCommandHandler。
原始更新的README
基于小命名约定的PHP CQRS库(松散基于LiteCQRS for C#),它依赖于MessageBus、Command、EventSourcing和Domain Event模式。
术语
CQS是命令-查询分离:一个范式,其中读取方法永远不会改变状态,写入方法永远不会返回数据。在此基础上,CQRS建议将读取模型和写入模型分离,并使用领域事件模式来通知读取模型关于写入模型的变化。
Glow使用命令模式和中央消息总线服务,该服务查找相应的处理器来执行命令。命令必须实现Glow\Commanding\Command
。它应该是一个简单的DTO(数据传输对象),仅包含一些描述它的属性(鼓励不可变性)。
在这个对象作为参数传递给CommandBus的handle(Command $command)
方法后,CommandBus找到适当的CommandHandler,并在其上调用相同的方法和相同的参数。强制约定是,对于每个XxxCommand,必须存在XxxCommandHandler(实现CommandHandler)。
在命令执行过程中,可以触发领域事件。这些事件也是一些简单的类,包含一些属性,它们可以可选地实现Glow\DomainEvent
。
事件队列知道在命令执行过程中触发了哪些领域事件,然后将它们发布到事件消息总线,许多监听器可以监听它们。
更改
约定
- 每个XxxCommand DTO在XxxCommand实现Command且XxxCommandHandler实现CommandHandler时映射到XxxCommandHandler。
- 领域事件应用于事件处理器“Event Class Shortname”=>“onEventClassShortname”。只有匹配时才会注册事件监听器。
- 领域事件应用于实体/聚合根“Event Class Shortname”=>“applyEventClassShortname”
- 你可以选择扩展
DefaultDomainEvent
,它有一个构造函数,将数组输入映射到属性,并在传递未知属性时抛出异常。 - 还有一个与
DefaultDomainEvent
具有相同语义的DefaultCommand
。扩展这个不是必需的。
示例
GreetingCommand implements Command
映射到注册的实现了CommandHandler的GreetingCommandHandler上的handle(GreetingCommand $command)
方法。HelloWorld\GreetedEvent
被传递到所有具有方法onGreeted(GreetedEvent $event)
的事件处理器。HelloWorld\Events\Greeted
被传递到所有具有方法onGreeted(Greeted $event)
的事件处理器。- 当在聚合根上创建时,
HelloWorld\GreetedEvent
会被委托到applyGreeted($event)
。
安装 & 要求
使用Composer安装
composer require lidskasila/glow
工作流程
以下是在执行过程中命令通常通过Glow堆栈采取的步骤
- 您将命令推送到一个
CommandBus
。命令是由您创建的简单对象,实现了Command
。 CommandBus
检查是否有可以执行您命令的处理程序。每个命令恰好有一个处理程序。- 命令处理程序改变领域模型的状态。它是通过创建事件(表示状态变化)并将它们传递到领域对象的
AggregateRoot::apply()
或DomainEventProvider::raise()
方法来做到的。 - 当命令完成后,命令总线将检查身份映射中的所有对象以查找事件。
- 找到的所有事件都将传递到
EventMessageBus#publish()
方法。 - 事件消息总线将所有事件派送到观察事件处理器。
- 事件处理器可以使用
CommandBus
再次创建新命令。
命令和事件处理器执行可以包装在管理事务的处理程序中。事件处理始终在任何命令事务之外触发。如果命令失败并抛出任何异常,则命令创建的所有事件都会被遗忘/忽略。在这种情况下不会触发任何事件处理器。
对于内存中的命令总线和企业消息总线,Glow确保命令和事件处理器的执行永远不会嵌套,而是按顺序线性化。这防止了每个命令的独立事务相互影响。
#TODO以下未更新
示例
请参阅examples/中的示例
example1.php
显示了使用一个领域对象的命令和事件消息总线example2_event.php
显示了在命令内直接使用事件消息总线example3_sequential_commands.php
演示了命令是如何按顺序处理的。tictactoe.php
使用CQRS实现了一个井字棋游戏。SymfonyExample.md
显示了在Symfony2项目范围内实现example1.php
设置
- 内存中的命令处理器,无事件发布/观察
<?php $userService = new UserService(); $commandBus = new DirectCommandBus() $commandBus->register('MyApp\ChangeEmailCommand', $userService);
- 内存中的命令和事件处理器
这使用Glow\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);
- 内存中的命令 + 自定义事件队列
Glow通过询问Glow\Bus\EventQueue
来了解触发的事件。提供自己的实现以独立于领域对象实现EventProviderInterface
。
<?php $messageBus = new InMemoryEventMessageBus(); $queue = new MyCustomEventQueue(); $commandBus = new DirectCommandBus(array( new EventMessageHandlerFactory($messageBus, $queue) ));
使用
实现您应用程序的用例
- 创建一个命令对象,接收所有必要的输入值。使用公共属性并扩展
Glow\DefaultCommand
以简化。 - 在您的任何服务(命令处理器)中添加一个新方法,其名称与命令相同
- 将命令处理器注册到命令总线以处理给定的命令。
- 让您的实体实现
Glow\AggregateRoot
或Glow\DomainEventProvider
- 使用受保护方法
raise(DomainEvent $event)
或apply(DomainEvent $event)
将事件附加到您的聚合根对象。
这就是简单的用例的全部内容。
如果你的命令触发了监听器检查的事件,你应该
- 创建一个特定领域的事件类。使用公共属性以简化。
- 创建事件处理器或向现有事件处理器中添加方法。
虽然为每个用例创建命令和事件似乎“复杂”,但这些对象实际上非常简单,只包含公共属性。使用你的IDE或编辑器功能,你可以轻松地立即生成它们。反过来,它们会使你的代码非常明确。
apply() 和 raise() 的区别
有两种方法可以将事件发布到外部世界。
DomainEventProvider#raise(DomainEvent $event)
是简单的,它只发出一个事件,不做其他任何事情。AggregateRoot#apply(DomainEvent $event)
要求你添加一个方法apply$eventName($event)
,它可以用来在对象上重新播放事件。这用于从事件中重新播放对象。
如果你不使用事件源,那么你只需使用 raise()
并完全忽略 apply()
就可以了。
失败的事件
EventMessageBus 阻止异常向上冒泡。为了允许对失败的事件处理器执行进行一些调试,有一个特殊的事件“EventExecutionFailed”,你可以监听它。你将传递一个包含属性 $exception
、$service
和 $event
的 Glow\Bus\EventExecutionFailed
实例,以便分析应用程序中的失败。
扩展点
你应该实现自己的 CommandBus
或扩展现有以将整个过程连接起来,使其完全符合你的工作需要。
插件
Symfony
在symfony内部,你可以通过注册带有 lite_cqrs.command_handler
或 lite_cqrs.event_handler
标签的服务来使用Glow。然后,这些服务将自动发现用于命令和事件。
命令和事件处理器是从Symfony依赖注入容器中懒加载的。
要启用包,请将以下内容放入你的Kernel中
new \Glow\Plugin\SymfonyBundle\GlowBundle(),
你可以通过添加以下内容到你的config.yml中来启用/禁用包
lite_cqrs: ~
请参阅 SymfonyExample.md 文档,以了解如何在Symfony2项目中使用Glow的完整演示。
Monolog
一个插件,使用 Monolog 记录每个命令和处理器的执行。它包括消息的类型和名称、其参数作为json以及其执行是否成功或失败。
Monolog集成到Symfony中注册了一个特定的通道 lite_cqrs
,你可以配置它以与Symfony中的默认通道不同。有关更多信息,请参阅 Symfony食谱。