CQRS AND EVENT SOURCING BASICS
Alexander Miertsch
Founder and CEO of prooph software GmbH
Founder and core contributor prooph components
contact@prooph.de
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 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 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,889