A tradução do artigo foi preparada para os alunos do curso profissional "Framework Laravel" 

1. Introdução
Este artigo é dedicado aos conceitos básicos da criação de sistemas CQRS de eventos na linguagem PHP e na estrutura do Laravel. Supõe-se que você esteja familiarizado com o esquema de desenvolvimento usando o barramento de comando e tenha uma idéia de eventos (em particular, a publicação de eventos para uma matriz de ouvintes). Para atualizar esse conhecimento, você pode usar o serviço Laracasts. Além disso, supõe-se que você tenha um certo entendimento do princípio do CQRS. Caso contrário, recomendo ouvir duas palestras: 
Workshop de Mathias Verraes sobre Geração de Eventos e 
CQRS e Geração de Eventos de Greg Young .
Não use o código fornecido aqui em seus projetos! É uma plataforma de aprendizado para entender as idéias por trás do CQRS. Esse código não pode ser considerado confiável, é mal testado e, além disso, raramente programa interfaces, portanto será muito mais difícil alterar partes individuais do código. Um exemplo muito melhor de um pacote CQRS que você pode usar é o 
Broadway, desenvolvido pelo Qandidate Lab . Este é um código limpo e pouco acoplado, no entanto, algumas abstrações não tornam totalmente claro se você nunca encontrou sistemas de eventos.
E o último - meu código está conectado aos eventos e ao barramento de comando do Laravel. Eu queria ver como o código ficaria no Laravel (eu costumo usar essa estrutura para projetos de pequenas agências); no entanto, olhando para trás, acho que devo criar minhas próprias implementações. Espero que meu código seja claro, mesmo para aqueles que não usam frameworks.
No github, o código está localizado em 
https://github.com/scazz/cqrs-tutorial.git e, em nosso guia, consideraremos seus componentes para aumentar a lógica.
Criaremos um sistema de registro inicial para uma escola de surf. Com sua ajuda, os clientes da escola podem se inscrever para as aulas. Para o processo de gravação, formulamos as seguintes regras:
- Cada lição deve ter pelo menos um cliente ...
- ... mas não mais que três.
Um dos recursos mais impressionantes dos sistemas CQRS baseados em eventos é a criação de modelos de leitura específicos para cada métrica necessária ao sistema. Você encontrará exemplos de projeção de modelos de leitura no ElasticSearch, e Greg Young implementou uma linguagem orientada a assuntos em seu armazenamento de eventos para lidar com eventos complexos. No entanto, para simplificar, nossa projeção de leitura será um banco de dados SQL padrão para uso com o Eloquent. Como resultado, teremos uma tabela para classes e outra para clientes.
O conceito de geração de eventos também permite processar eventos offline. Mas neste artigo, aderirei ao máximo aos modelos de desenvolvimento “tradicionais” (novamente por simplicidade), e nossas projeções de leitura serão atualizadas em tempo real, imediatamente após os eventos serem armazenados na loja.
Configuração do projeto e primeiro teste
git clone https://github.com/scazz/cqrs-tutorial 
Crie um novo projeto Laravel 5
 $> laravel new cqrs-tutorial 
E, para começar, precisamos de um teste. Usaremos o teste de integração, que garantirá que o registro do cliente para as aulas leve ao fato de que a lição é criada em nosso modelo Eloquent.
Listando tests / CQRSTest.php:
 use Illuminate\Foundation\Bus\DispatchesCommands; class CQRSTest extends TestCase { use DispatchesCommands; 
 /** *  ,   BookLesson       * @return void */ public function testFiringEventUpdatesReadModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440000'; $clientName = "George"; $lessonId = new LessonId($testLessonId); $command = new BookLesson($lessonId, $clientName); $this->dispatch($command); $this->assertNotNull(Lesson::find($testLessonId)); $this->assertEquals( Lesson::find($testLessonId)->clientName, $clientName ); } } 
Pré-atribuímos a nova atividade a um ID, criamos uma equipe para se inscrever em uma nova atividade e pedimos ao Laravel para enviá-la. Na tabela de lições, precisamos criar um novo registro que possamos ler usando o modelo Eloquent. Como precisamos de um banco de dados, preencha seu arquivo .env corretamente.
Cada evento gravado em nosso armazenamento de eventos é anexado à raiz do agregado, que chamaremos simplesmente de entidade (Entidade) - uma abstração para fins educacionais apenas adiciona confusão. O ID é um identificador exclusivo universal (UUID). O armazenamento de eventos não se importa se o evento se aplica à lição ou ao cliente. Ele só sabe que está associado a um ID.
Com base nos erros identificados durante o teste, podemos criar classes ausentes. Primeiro, criaremos a classe LessonId, depois o comando BookLesson (não se preocupe com o método manipulador ainda, apenas continue executando o teste). A classe Lesson é um modelo de leitura fora do espaço de nomes da lição. Um modelo de leitura exclusivo - a lógica da área de assunto nunca será armazenada aqui. Em conclusão, precisaremos criar uma migração para a tabela de lições.
Para manter a clareza do código, usarei a biblioteca de verificação de asserções. Pode ser adicionado com o seguinte comando:
 $> composer require beberlei/assert 
Considere o processo que deve ser iniciado por este comando:
- Validação: comandos imperativos podem falhar e os eventos já ocorreram e, portanto, não devem falhar.
- Crie um novo evento LessonWasBooked (inscrito em uma lição).
- Atualize o status da atividade. (O modelo de registro deve estar ciente do estado do modelo para poder executar a validação.)
- Inclua este evento no fluxo de eventos não confirmados armazenados no modelo de registro de atividade.
- Salve o fluxo de eventos não confirmados no repositório.
- Aumente o evento LessonWasBooked globalmente para informar todos os projetores de leitura para atualizar a tabela de lições.
Primeiro, você precisa criar um modelo de gravação para a lição. Usaremos o método de fábrica estático 
Lesson::bookClientOntoNewLesson() . Ele gera um novo evento 
LessonWasOpened (a lição está aberta), aplica esse evento a si mesmo (apenas define seu ID), adiciona o novo evento à lista de eventos não confirmados na forma de 
DomainEventMessage (o evento mais alguns metadados que usamos ao salvar no armazenamento de eventos).
O processo é repetido para adicionar um cliente ao evento. Ao aplicar o evento 
ClientWasBookedOntoLesson (o cliente foi inscrito na lição), o modelo de gravação não rastreia os nomes dos clientes, mas apenas o número de clientes registrados. Os modelos de registro não precisam saber os nomes dos clientes para garantir consistência.
Os métodos 
applyClientWasBookedOntoLesson e 
applyClientWasBookedOntoLesson podem parecer um pouco estranhos 
applyClientWasBookedOntoLesson momento. Nós os usaremos mais tarde quando precisarmos reproduzir eventos antigos para formar o estado do modelo de gravação. Não é fácil de explicar, por isso darei um código que ajudará você a entender esse processo. Mais tarde, extrairemos o código que processa eventos não confirmados e gera mensagens de eventos do domínio.
 app/School/Lesson/Lesson.php public function openLesson( LessonId $lessonId ) { /*      ,      ,       */ $this->apply( new LessonWasOpened( $lessonId) ); } protected function applyLessonWasOpened( LessonWasOpened $event ) { $this->lessonId = $event->getLessonId(); $this->numberOfClients = 0; } public function bookClient( $clientName ) { if ($this->numberOfClients >= 3) { throw new TooManyClientsAddedToLesson(); } $this->apply( new ClientBookedOntoLesson( $this->lessonId, $clientName) ); } /** *       — *  ,       *      ,       , *      . */ protected function applyClientBookedOntoLesson( ClientBookedOntoLesson $event ) { $this->numberOfClients++; } 
Podemos extrair os componentes do CQRS do nosso modelo de gravação - fragmentos da classe envolvidos no processamento de eventos não confirmados. Também podemos limpar a API de uma entidade gerada por evento, criando uma função 
apply() segura que aceita o evento, chama o método 
applyEventName() correspondente e adiciona um novo evento 
DomainEventMessage à lista de eventos não confirmados. A classe extraída é um detalhe da implementação do CQRS e não contém lógica de domínio; portanto, podemos criar um novo espaço para nome: App \ CQRS:
Preste atenção ao código app/CQRS/EventSourcedEntity.php 
Para que o código funcione, precisamos adicionar a classe DomainEventMessage , que é um DTO simples - ele pode ser encontrado em app/CQRS/DomainEventMessage.phpAssim, obtivemos um sistema que gera eventos para cada tentativa de gravação e usa eventos para registrar as alterações necessárias para evitar invariantes. O próximo passo é salvar esses eventos na loja (EventStore). Primeiro de tudo, esse repositório de eventos precisa ser criado. Para simplificar, usaremos o modelo Eloquent, uma tabela SQL simples com os seguintes campos: * 
UUID (para saber a qual entidade aplicar o evento) * 
event_payload (mensagem serializada contendo tudo o necessário para recriar o evento) * 
recordedAt - timestamp para saber quando o evento aconteceu. Se você revisar cuidadosamente o código, verá que eu criei dois comandos - para criar e destruir nossa tabela de armazenamento de eventos:
- php artisan eloquenteventstore: create (App \ CQRS \ EloquentEventStore \ CreateEloquentEventStore)
- php artisan eloquenteventstore: drop (App \ CQRS \ EloquentEventStore \ DropEloquentEventStore) (não esqueça de adicioná-los ao App \ Console \ Kernel.php para que eles carreguem).
Há dois motivos muito bons para não usar o SQL como um armazenamento de eventos: ele não implementa o modelo somente de acréscimo (apenas a adição de dados, os eventos devem ser imutáveis) e também porque o SQL não é uma linguagem de consulta ideal para bancos de dados temporais. Programamos a interface para facilitar a substituição do armazenamento de eventos nas publicações subsequentes.
Para salvar eventos, use o repositório. Sempre que 
save() é chamado para o modelo de gravação, salvamos a lista de eventos não confirmados no armazenamento de eventos. Para armazenar eventos, precisamos de um mecanismo para serialização e desserialização. Crie um serializador para isso. Precisamos de metadados, como uma classe de evento (por exemplo, 
App\School\Lesson\Events\LessonWasOpened ) e uma carga útil de evento (dados necessários para reconstruir um evento).
Tudo isso será codificado no formato JSON e, em seguida, gravado em nosso banco de dados junto com a entidade UUID e o registro de data e hora. Queremos atualizar nossos modelos de leitura após a captura de eventos, para que o repositório acione todos os eventos após salvar. O serializador será responsável por escrever a classe do evento, enquanto o evento será responsável por serializar sua carga útil. Um evento totalmente serializado será mais ou menos assim:
  { class: "App\\School\\Lesson\\Events\\", event: $event->serialize() } 
Como todos os eventos exigem um método de serialização e desserialização, podemos criar uma interface 
SerializableEvent e adicionar uma indicação do tipo de valor esperado. Atualize nosso evento 
LessonWasOpened :
 app/School/Lesson/Events/LessonWasOpened.php class LessonWasOpened implements SerializableEvent { public function serialize() { return array( 'lessonId'=> (string) $this->getLessonId() ); } } 
Crie um repositório 
LessonRepository . Podemos refatorar e extrair os componentes principais do CQRS posteriormente.
app/School/Lesson/LessonRepository.php eventStoreRepository = new EloquentEventStoreRepository( new EventSerializer() ); } public function save(Lesson $lesson) { /** @var DomainEventMessage $domainEventMessage */ foreach( $lesson->getUncommittedDomainEvents() as $domainEventMessage ) { $this->eventStoreRepository->append( $domainEventMessage->getId(), $domainEventMessage->getEvent(), $domainEventMessage->getRecordedAt() ); Event::fire($domainEventMessage->getEvent()); } } } 
Se você executar o teste de integração novamente e depois verificar a tabela SQL 
domain_events , deverá ver dois eventos no banco de dados.
Nossa etapa final para passar com êxito no teste é ouvir os eventos de transmissão e atualizar a projeção do modelo de leitura da lição. Os eventos de transmissão da lição serão interceptados pelo 
LessonProjector , que aplicará as alterações necessárias no 
LessonProjection (modelos eloquentes da tabela de lições):
  app/School/Lesson/Projections/LessonProjector.php class LessonProjector { public function applyLessonWasOpened( LessonWasOpened $event ) { $lessonProjection = new LessonProjection(); $lessonProjection->id = $event->getLessonId(); $lessonProjection->save(); } public function subscribe(Dispatcher $events) { $fullClassName = self::class; $events->listen( LessonWasOpened::class, $fullClassName.'@applyLessonWasOpened'); } }  app/School/Lesson/Projections/LessonProjection.php class LessonProjection extends Model { public $timestamps = false; protected $table = "lessons"; } 
Se você executar o teste, verá que ocorreu um erro SQL:
 Unknown column 'clientName' in 'field list' 
Assim que criarmos uma migração para adicionar 
clientName à tabela de lições, passaremos no teste com êxito. Implementamos a funcionalidade básica do CQRS: as equipes criam eventos que são usados para gerar modelos de leitura.
Melhorando o modelo de leitura com links
Atingimos um marco significativo, mas isso não é tudo! Até agora, o modelo de leitura suporta apenas um cliente (nós especificamos três em nossas regras de domínio). As alterações que fazemos no modelo de leitura são bastante simples: apenas criamos um modelo de projeção do cliente e um 
ClientProjector que captura o evento 
ClientBookedOntoLesson . Primeiro, atualizamos nosso teste para refletir as alterações que queremos ver em nosso modelo de leitura:
 tests/CQRSTest.php public function testFiringEventUpdatesReadModel() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $clientName = "George"; $command = new BookLesson($lessonId, $clientName); $this->dispatch($command); $lesson = Lesson::find( (string) $lessonId); $this->assertEquals( $lesson->id, (string) $lessonId ); $client = $lesson->clients()->first(); $this->assertEquals($client->name, $clientName); } 
Esta é uma demonstração clara de como é fácil alterar os modelos de leitura. Tudo, até o repositório de eventos, permanece inalterado. Como bônus, ao usar o sistema de eventos, obtemos dados para testes básicos - ao trocar o projetor do modelo de leitura, ouvimos todos os eventos que já aconteceram em nosso sistema.
Reproduzimos esses eventos usando o novo projetor, verificamos exceções e comparamos os resultados com as projeções anteriores. Depois que o sistema estiver funcionando há algum tempo, teremos uma seleção bastante representativa de eventos para testar nossos projetores.
Atualmente, nosso modelo de gravação não tem capacidade para carregar o estado atual. Se quisermos adicionar um segundo cliente à lição, podemos simplesmente criar um segundo evento ClientWasAddedToLesson, mas não podemos fornecer proteção contra invariantes. Para maior clareza, proponho escrever um segundo teste simulando a gravação de dois clientes por aula.
 tests/CQRSTest.php public function testLoadingWriteModel() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $clientName_1 = "George"; $clientName_2 = "Fred"; $command = new BookLesson($lessonId, $clientName_1); $this->dispatch($command); $command = new BookClientOntoLesson($lessonId, $clientName_2); $this->dispatch($command); $lesson = Lesson::find( (string) $lessonId ); $this->assertClientCollectionContains($lesson->clients, $clientName_1); $this->assertClientCollectionContains($lesson->clients, $clientName_2); } 
Para o nosso modelo de gravação, precisamos implementar um método de "carregar" uma entidade para a qual os eventos já se aplicam a ela no armazenamento de eventos. Podemos conseguir isso reproduzindo todos os eventos que se referem ao UUID da entidade. Em termos gerais, o processo é o seguinte:
- Recebemos todas as mensagens de eventos relevantes do armazenamento de eventos.
- Para cada mensagem, recriamos o evento correspondente.
- Criamos um novo modelo de registro de entidade e reproduzimos cada evento.
No momento, nossos testes lançam exceções, portanto, começaremos criando o 
BookClientOntoLesson necessário (registrar um cliente para a lição), usando o comando 
BookLesson como modelo. O método manipulador terá a seguinte aparência:
  app/School/Lesson/Commands/BookClientOntoLesson.php public function handle(LessonRepository $repository) { /** @var Lesson $lesson */ $lesson = $repository->load($this->lessonId); $lesson->bookClient($this->clientName); $repository->save($lesson); }      : app/School/Lesson/LessonRepository.php public function load(LessonId $id) { $events = $this->eventStoreRepository->load($id); $lesson = new Lesson(); $lesson->initializeState($events); return $lesson; } 
A função de carregamento do repositório retorna uma matriz de eventos recriados. Para fazer isso, ela primeiro encontra mensagens sobre eventos no repositório e depois as transmite ao 
Serializer para converter cada mensagem em um evento. 
Serializer cria mensagens a partir de eventos, portanto, precisamos adicionar o método 
deserialize() para executar a transformação inversa. Lembre-se de que o 
Serializer é passado para cada evento para serializar os dados do evento (por exemplo, o nome do cliente). Faremos o mesmo para executar a transformação inversa, enquanto nossa interface 
SerializableEvent deve ser atualizada usando o método 
deserialize() . Vejamos o código para que tudo se encaixe. A primeira é a 
EventStoreRepository carregamento 
EventStoreRepository :
 app/CQRS/EloquentEventStore/EloquentEventStoreRepository.php public function load($uuid) { $eventMessages = EloquentEventStoreModel::where('uuid', $uuid)->get(); $events = []; foreach($eventMessages as $eventMessage) { /*       event_payload,        . */ $events[] = $this->eventSerializer->deserialize( json_decode($eventMessage->event_payload)); } return $events; } 
Usando a função de desserialização apropriada no 
eventSerializer :
 app/CQRS/Serializer/EventSerializer.php public function serialize( SerializableEvent $event ) { return array( 'class' => get_class($event), 'payload' => $event->serialize() ); } public function deserialize( $serializedEvent ) { $eventClass = $serializedEvent->class; $eventPayload = $serializedEvent->payload; return $eventClass::deserialize($eventPayload); } 
Em conclusão, usaremos o método estático de fábrica 
deserialize() em 
LessonWasOpened (precisamos adicionar esse método a cada evento)
 app/School/Lesson/Events/LessonWasOpened.php public static function deserialize($data) { $lessonId = new LessonId($data->lessonId); return new self($lessonId); } 
Agora temos uma matriz de todos os eventos que acabamos de reproduzir em relação ao nosso modelo de registro de entidade para inicializar o estado no método 
initializeState em 
app/CQRS/EventSouredEntity.phpAgora execute nosso teste. Bingo!
De fato, no momento não temos um teste para verificar a conformidade com nossas regras de domínio, então vamos escrever:
 tests/CQRSTest.php public function testMoreThan3ClientsCannotBeAddedToALesson() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $this->dispatch( new BookLesson($lessonId, "bob") ); $this->dispatch( new BookClientOntoLesson($lessonId, "george") ); $this->dispatch( new BookClientOntoLesson($lessonId, "fred") ); $this->setExpectedException( TooManyClientsAddedToLesson::class ); $this->dispatch( new BookClientOntoLesson($lessonId, "emma") ); } 
Observe que precisamos apenas de 
lessonId - esse teste reinicializa o estado da lição durante cada comando.
No momento, simplesmente transferimos 
UUID criados manualmente, enquanto na realidade queremos gerá-los automaticamente. Vou usar o pacote 
Ramsy\UUID , então vamos instalá-lo com o 
composer :
 $> composer require ramsey/uuid 
Agora atualize nossos testes para usar o novo pacote:
 tests/CQRSTest.php public function testEntityCreationWithUUIDGenerator() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $this->dispatch( new BookLesson($lessonId, "bob") ); $this->assertInstanceOf( Lesson::class, Lesson::find( (string) $lessonId) ); } 
Agora, o novo desenvolvedor de projeto pode examinar o código, consulte 
App\School\ReadModels , que contém um conjunto de modelos Eloquent, e usar esses modelos para gravar alterações na tabela de lições. Podemos evitar isso criando uma classe 
ImmutableModel que estende a classe Eloquent Model e substitui o método save em 
app/CQRS/ReadModelImmutableModel.php .