CQRS AND EVENT SOURCING BASICS

Alexander Miertsch

Founder and CEO of prooph software GmbH

Founder and core contributor prooph components

 

 

contact@prooph.de

@codeliner

@prooph_software

prooph components

CQRS and Event Sourcing packages for PHP

Supported Web Frameworks

or any other framework/codebase supporting container-interop & interop-config

prooph team

Community

Join prooph

Problem Space

Complex

Distributed

Chaotic

When to use CQRS & ES ?

Domain Events

to the rescue

 

Informational message

Communicate facts that have happened

Relevant to the business

 

Event Storming

Deeper insights with events

Event Flow

record, publish, react

OrderPlaced

OrderPayed

OrderShipped

Change State, Then Publish

ORM - Publisher

<?php

final class OrderService
{
    private $entityManager;
    private $orderRepositoy;
    private $eventPublisher;

    public function placeOrder(string $orderId, OrderItem ...$items): void
    {
        $order = $this->orderRepostoy->get($orderId);
        $this->entityManager->beginTransaction();
        
        try {
            $order->place($items);
            $this->entityManager->commit();
        } catch(\Throwable $error) {
            $this->entityManager->rollBack();
            throw $error;
        }
        
        $event = OrderPlaced::fromOrder($order);
        $this->eventPublisher->publish($event);        
    }
}

Record Event, Then Publish

Event Store - Publisher

<?php

final class PlaceOrderHandler
{
    private $eventStore;
    private $eventPublisher;

    public function handle(PlaceOrder $command): void
    {
        //Note: Loading/saving is normally done by event sourcing repository
        $orderStream = new StreamName('order-'.$command->orderId());
        $history = $this->eventStore->load($orderStream);
        
        $order = Order::reconstituteFromHistory($history);
        $newEvents = $order->place($command->items());
        
        //Atomic operation
        $this->eventStore->appendTo($orderStream, $newEvents);
                
        $this->eventPublisher->publish($newEvents);        
    }
}

Event Sourcing

State is a left fold of past events

# E1 at t1: UserRegistered
userId: 1
name: John Doe
email: doe@fake.com

# E2 at t2: UsernameChanged
userId: 1
oldName: John Doe
newName: Alex

# E3 at t3: EmailChanged
userId: 1
oldEmail: doe@fake.com
newEmail: contact@prooph.de

_______________________________________________

# current state (t:now) = ((E1 + E2) + E3):
userId: 1
name: Alex
email: contact@prooph.de
version: 3

Time Travel

Debug like a boss

UserRegistered

UsernameChanged

ChangeEmailFailed

{

E1 + E2

Change Email

Temporal Queries

Use time interval as query param

UserRegistered

UsernameChanged

5 min

7 min

Who has changed username within 5 min after registration?

UserRegistered

UsernameChanged

Event Sourcing in PHP

prooph/

event-sourcing

Event sourcing basics

Event store integration

 

prooph/

event-store

Append-only streams

Persistent projections

Event stream queries

prooph components

CQRS

SRP on system level

Gateway

Write Model

Read Model

CQRS and Event Sourcing

Double Team

Gateway

Write Model

Read Model

Event Store

Projection

Cache

CQRS in PHP

prooph/

service-bus

Lightweight message bus

Supporting CQRS

 

prooph/

psb-*-producer

Messaging adapters

Async processing

RPC support

prooph components

Just Try It

Service Bus for Symfony 

prooph_service_bus:
  command_buses:
    todo_command_bus:
      router:
        type: 'prooph_service_bus.command_bus_router'

  event_buses:
    todo_event_bus:
      plugins:
        - 'prooph_service_bus.on_event_invoke_strategy'
      router:
        type: 'prooph_service_bus.event_bus_router'
        routes:
          'Prooph\ProophessorDo\Model\Todo\Event\TodoAssigneeWasReminded':
            - 'Prooph\ProophessorDo\ProcessManager\SendTodoReminderMailSubscriber'

          'Prooph\ProophessorDo\Model\Todo\Event\TodoWasMarkedAsExpired':
            - 'Prooph\ProophessorDo\ProcessManager\SendTodoDeadlineExpiredMailSubscriber'

Tagged Command Handler

Prooph\ProophessorDo\Model\Todo\Handler\MarkTodoAsDoneHandler:
        arguments: ['@todo_list']
        public: true
        tags:
            - { 
                name: 'prooph_service_bus.todo_command_bus.route_target', 
                message_detection: true 
              }

Handle Commands



class MarkTodoAsDoneHandler
{
    /**
     * @var TodoList
     */
    private $todoList;

    public function __construct(TodoList $todoList)
    {
        $this->todoList = $todoList;
    }

    public function __invoke(MarkTodoAsDone $command): void
    {
        $todo = $this->todoList->get($command->todoId());
        if (! $todo) {
            throw TodoNotFound::withTodoId($command->todoId());
        }
        $todo->markAsDone();
        $this->todoList->save($todo);
    }
}

Event Store for Symfony 



services:
    Prooph\EventStore\Pdo\MySqlEventStore:
        arguments:
            - '@prooph_event_store.message_factory', 
            - '@doctrine.pdo.connection', 
            - '@prooph_event_store.single_stream_strategy'

    doctrine.pdo.connection:
        class: PDO
        factory: ['@database_connection', getWrappedConnection]

    prooph_event_store.single_stream_strategy:
        class: Prooph\EventStore\Pdo\PersistenceStrategy\MySqlSingleStreamStrategy

    prooph_event_sourcing.aggregate_translator:
        class: Prooph\EventSourcing\EventStoreIntegration\AggregateTranslator

Event Store for Symfony 



prooph_event_store:
  stores:
    todo_store:
      event_store: Prooph\EventStore\Pdo\MySqlEventStore
        repositories:
          todo_list:
            repository_class: Infrastructure\Repository\EventStoreTodoList
            aggregate_type: Model\Todo\Todo
            aggregate_translator: prooph_event_sourcing.aggregate_translator

                

Event Store for Symfony 

final class Todo extends AggregateRoot implements Entity
{
    //...

    /**
     * @throws Exception\TodoNotOpen
     */
    public function markAsDone(): void
    {
        $status = TodoStatus::DONE();
        if (! $this->status->is(TodoStatus::OPEN())) {
            throw Exception\TodoNotOpen::triedStatus($status, $this);
        }
        $this->recordThat(
            TodoWasMarkedAsDone::fromStatus(
                $this->todoId, 
                $this->status, 
                $status, 
                $this->assigneeId
            )
        );
    }
    //...
}

                

Event Store for Symfony 

Event Sourcing Tutorial 

Thanks for listening

Questions?

Symfony Live Berlin 2017 CQRS and ES Basics

By prooph

Symfony Live Berlin 2017 CQRS and ES Basics

  • 3,786