Developing event-driven microservices with event sourcing and CQRS (Shanghai)
CQRS + Event Sourcing in PHP
-
Upload
manuel-lopez-torrent -
Category
Internet
-
view
548 -
download
5
Transcript of CQRS + Event Sourcing in PHP
CQRSCommand Query Responsibility Segregation
EDUMENT
http://cqrs.nu
DDD in PHP
https://github.com/dddinphp/blog-cqrs
https://github.com/dddinphp/last-wishes-gamify
¿CQRS?
CQRSPatrón de diseño de app
Lecturas / Escrituras
Modelo rico
Mejor rendimiento
Mejor escalabilidad
Objetivos
Greg Younghttps://goodenoughsoftware.net/@gregyoung
DDD (Domain Driven Design)
Modelo rico VS Modelo Anémico
Patrones tácticos
Arquitectura de capas
Arquitectura hexagonal
Requisitos
Cafetería
- Cuandos los clientes entran en el café se sientan en una mesa , un camarero/a , abre una cuenta para esa mesa
- Los clientes pueden ordenar bebida y/o comidas del menú
- Una vez ordenadas las bebidas pueden ser servidas de inmediato.
- La comida debe ser preparada en cocina. Una vez se ha preparado puede ser servida
- Cuando los clientes terminan de comer pagan la cuenta ( pueden dejar propina ) y la cuenta se cierra
- No se puede cerrar una cuenta si hay bebida o comida pendientes
Abrir
Ordenar
Preparar
Servir
Pagar
Cerrar
Camarero
Mesa
Cuenta
Bebida / Comida
Trasladar el modelo a objetos ( DDD táctico )
Creamos tests para asegurar su corrección
Arquitectura hexagonal para conectar el modelo con el mundo
exterior
Application Layer
DomainModel
ApplicationService 1
Request
InfrastructureLayer DataStoreApplication
Service 2
ApplicationService n
Response
<?php
class Tab
{
static public function open($table, $waiter): Tab {}
public function placeOrder($orderedItems) {}
public function serveDrinks($drinksServed) {}
public function prepareFood($foodPrepared) {}
public function serveFood($foodServed) {}
public function close(float $amount) {}
}
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
}
<?php
class OrderedItem
{
public function __construct(int $menuNumber, bool $IsDrink, float $price) {}
public function getMenuNumber(): int {}
public function isDrink(): bool {}
public function getPrice(): float {}
}
interface OrderedItemsRepository
{
public function findById($id): OrderedItem;
}
OK
Nuevos requisitos
Maître
Barman
Cocina
<?php
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
}
<?php
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
public function getTabsByWaiter($waiter);
public function getTabsWithDrinkPending();
public function getTabsWithFoodPrepared();
public function getTabsOpen();
public function getTabsClosed(DateTime $date);
}
<?php
use Doctrine\ORM\Query
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
public function getTabsByQuery(Query $query)
}
<?php
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
public function getTabsBySpeficication(Specification $s);
}
<?php
class MysqlTabRepository implements TabRepository { ... }
class RedisTabRepository implements TabRepository { ... }
class MongoDBTabRepository implements TabRepository { ... }
¿?
ORM
Frameworks
Contaminamos el modelo
SRP
SRP
Command Query Responsibility Segregation
Read Model Write Model
Read Model Write Model
Lógica de negocio
Read Model Write Model
Query Command
Commands
OpenTab
PlaceOrder
MarkDrinksServed
MarkFoodPrepared
MarkFoodServed
CloseTab
Querys
AllTabs
OneTab
AllTabsByWaiter
InfrastructureLayer
Application Layer
CommandHandlerCommand
CommandHandler
QueryHandle
Read Model
Model
Command
QueryDataStore
Response
QueryHandle
Query
Response
Controladores Comando Bus Command Handler
CommandCommandHandler
class OpenTabCommand
{
private $tabId;
private $tableNumber;
private $waiterId;
public function __construct($tabId, $tableNumber, $waiterId)
{
$this->tabId = $tabId;
$this->tableNumber = $tableNumber;
$this->waiterId = $waiterId;
}
public function getTabId() { }
public function getTableNumber() { }
public function getWaiterId() { }
}
class OpenTabHandler
{
private $tabRepopsitory;
public function __construct(TabRepository $tabRepository)
{
$this->tabRepopsitory = $tabRepository;
}
public function handle(OpenTabCommand $command)
{
$newTab = Tab::openWithId(
TabId::fromString($command->getTabId()),
$command->getTableNumber(),
$command->getWaiterId()
);
$this->tabRepopsitory->add($newTab);
}
}
QueryQueryHandler
class OneTabQuery
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class OneTabQueryHandler
{
private $tabsRepository;
private $dataTransformer;
public function __construct(
TabsRepository $tabsRepostiory,
DataTranformer $dataTransformer
) {
$this->tabsRepository = $tabsRepostiory;
$this->dataTransformer = $dataTransformer;
}
public function handle(OneTabQuery $query)
{
$tab = $this->tabsRepository->find($query->id);
$this->dataTransformer->write($tab);
return $this->dataTransformer->read();
}
}
Modelo Escritura
Tab
TabRepository
OrderedItem
OrderedItemRepository
Modelo Lectura
TabView
TabViewRepository
OrderedItemView
OrderedItemViewRepository
Modelo Escritura
Tab
TabRepository
OrderedItem
OrderedItemRepository
Modelo Lectura
TabView
TabViewRepository
OrderedItemView
OrderedItemViewRepository
Lógica neg
ocio Modelo
Anémico
<?php
interface TabRepository
{
public function getById(TabId $tabId);
public function save(Tab $tab);
}
interface TabViewRepository
{
public function getTabsByWaiter($waiter);
public function getTabsWithDrinkPending();
public function getTabsWithFoodPrepared();
public function getTabsOpen();
public function getTabsClosed(DateTime $date);
}
Modelo rico
Mejor rendimiento
Mejor escalabilidad
¿Mejor rendimiento?
InfrastructureLayer
Application Layer
CommandHandlerCommand
CommandHandler
QueryHandle
Read Model
Model
Command
QueryDataStore
Response
QueryHandle
Query
Response
Model InfrastructureLayer
DataStoreInfrastructure
Layer
DataStore
Application Layer
CommandHandlerCommand
CommandHandler
QueryHandle
Read Model
Command
Query
Response
QueryHandle
Query
Response
<?php
class RedisTabRepository implements TabRepository { ... }
class MysqlTabViewRepository implements TabViewRepository { ... }
Modelo rico
Mejor rendimiento
Mejor escalabilidad
¿Mejor escalabilidad?
Model InfrastructureLayer
DataStoreInfrastructure
Layer
DataStore
Application Layer
CommandHandlerCommand
CommandHandler
QueryHandle
Read Model
Command
Query
Response
QueryHandle
Query
Response
WRITE SERVICE
READ SERVICE
APIGATEWA
Y
READSERVICE
WRITESERVICE
READSERVICE
READSERVICE
Modelo rico
Mejor rendimiento
Mejor escalabilidad
¿Consistencia de los datos?
InfrastructureLayer
Application Layer
CommandHandlerCommand
CommandHandler
QueryHandle
Read Model
Model
Command
Query
DataStore
Response
QueryHandle
Query
Response
InfrastructureLayer
DataStore
Eventos del dominio
Se ha abierto una cuenta
Se ordenan bebidas y comida
La comida está preparada
Las bebidas se han servido
La comida se ha preparado
La comida se ha servidor
Se ha cerrado una cuenta
TabOpened
DrinksOrdered
FoodOrdered
DrinksServed
FoodPrepared
FoodServed
TabClosed
<?php
class DrinksOrdered extends TabEvent
{
private $items;
public function __construct(TabId $id, $items)
{
$this->id = $id;
$this->items = $items;
}
public function getItems()
{
return $this->items;
}
}
Command Model
Evento
BU
SModel
Evento
Evento
Model Evento Listenner
BU
S
DataStore
Model Listenner
BU
S
Event sourcing
El estado de nuestro sistema
es la suma de todos los eventos
ocurridos en el.
A1 {x = 1y = 2
}ID X Y
A1 1 2
A2 3 5 A2 {x = 3y = 5
}
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A2 {x = nully = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A1 {x = 1y = 7
}
A2 {x = nully = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A1 {x = 1y = 7
}
A2 {x = nully = null
}
A2 {x = 3y = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A1 {x = 1y = 7
}
A1 {x = 1y = 2
}
A2 {x = nully = null
}
A2 {x = 3y = null
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A1 {x = 1y = 7
}
A1 {x = 1y = 2
}
A2 {x = nully = null
}
A2 {x = 3y = null
}
A2 {x = 3y = 5
}
Tiempo
A2, y = 5
A1, y = 2
A2, x = 3
A1 , y = 7
A2, x = null y = null
A1 , x = 1
A1, x = null y = null
A1 {x = nully = null
}
A1 {x = 1y = null
}
A1 {x = 1y = 7
}
A1 {x = 1y = 2
}
A2 {x = nully = null
}
A2 {x = 3y = null
}
A2 {x = 3y = null
}
Historia
Auditoria
Fácil persistencia
Reconstruimos nuestros agregado a partir de un
flujo de eventos
Command Model
Evento
BU
SModel
Evento
Evento
EventStore
Proyecciones
ID X Y
A1 1 2
A2 3 5
A1, y = 2
A1 , y = 7
A1 , x = 1
A3, y = 5
A2, x = 3
ID X Y
A1 1 2
A2 3 5
A1, x = 2
A1, y = 2
A1 , y = 7
A1 , x = 1
A3, y = 5
A2, x = 3
ID X Y
A1 1 2
A2 3 5
UPDATE table_name SET x=2 WHERE id = A1 A1, x = 2
A1, y = 2
A1 , y = 7
A1 , x = 1
A3, y = 5
A2, x = 3
ID X Y
A1 2 2
A2 3 5
A1, x = 2
A1, y = 2
A1 , y = 7
A1 , x = 1
Inconsistencia Eventual
¿Dónde generamos los eventos?
Agregados
Entidad raíz
Id del agregado
Almacenar los eventos
Reconstruir desde flujo de eventos
<?php
class Tab
{
static public function open($table, $waiter): Tab {}
public function placeOrder($orderedItems) {}
public function serveDrinks($drinksServed) {}
public function prepareFood($foodPrepared) {}
public function serveFood($foodServed) {}
public function close(float $amount) {}
}
<?php
class Tab
{
// TabOpened
static public function open($table, $waiter): Tab {}
// DrinksOrdered , FoodOrdered
public function placeOrder($orderedItems) {}
// DrinksServed
public function serveDrinks($drinksServed) {}
// FoodPrepared
public function prepareFood($foodPrepared) {}
// FoodServed
public function serveFood($foodServed) {}
// TabClosed
public function close(float $amount) {}
}
<?php
class Tab {
static public function open($table, $waiter): Tab
{
$id = TabId::create();
$newTab = new Tab($id, $table, $waiter);
DomainEventPublisher::instance()->publish(
new TabOpened($id, $table, $waiter)
);
return $newTab;
}
}
<?php
class Tab {
static public function open($table, $waiter): Tab
{
$id = TabId::create();
$newTab = new Tab($id, $table, $waiter);
DomainEventPublisher::instance()->publish(
new TabOpened($id, $table, $waiter)
);
return $newTab;
}
}
<?php
class Tab extends Aggregate {
static public function open($table, $waiter): Tab
{
$id = TabId::create();
$newTab = new Tab($id, $table, $waiter);
$this->recordThat(new TabOpened($id, $table, $waiter));
return $newTab;
}
}
abstract class Aggregate implements AggregateRoot
{
private $recordedEvents = [];
protected function recordThat(DomainEvent $aDomainEvent)
{
$this->recordedEvents[] = $aDomainEvent;
}
public function getRecordedEvents(): DomainEvents
{
return new DomainEvents($this->recordedEvents);
}
public function clearRecordedEvents()
{
$this->recordedEvents = [];
}
}
abstract class Aggregate implements AggregateRoot
{
public static function reconstituteFrom(AggregateHistory $anAggregateHistory) {
$anAggregate = static::createEmptyWithId(
$anAggregateHistory->getAggregateId()
);
foreach ($anAggregateHistory as $anEvent) {
$anAggregate->apply($anEvent);
}
return $anAggregate;
}
private function apply($anEvent)
{
$method = 'apply' . ClassFunctions::short($anEvent);
$this->$method($anEvent);
}
}
class Tab extends Aggregate {
public function applyDrinksServed(DrinksServed $drinksServed)
{
array_walk($drinksServed->getItems(),
function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
}
}
class Tab extends Aggregate {
public function applyDrinksServed(DrinksServed $drinksServed)
{
array_walk($drinksServed->getItems(),
function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
}
}
Refactorizar agregados
<?php
class Tab {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
array_walk($drinksServed, function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
}
}
<?php
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
array_walk($drinksServed, function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
$this->recordThat(new DrinksServed(
$this->getAggregateId(),
$drinksServed
));
}
}
<?php
class Tab extends Aggregate {
public function applyDrinksServed(DrinksServed $drinksServed)
{
array_walk($drinksServed->getItems(),
function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
}
}
<?php
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
array_walk($drinksServed, function($drinkServedNumber) {
$item = $this->outstandingDrinks[$drinkServedNumber];
unset($this->outstandingDrinks[$drinkServedNumber]);
$this->servedItems[$drinkServedNumber] = $item;
});
$this->recordThat(new DrinksServed(
$this->getAggregateId(),
$drinksServed
));
}
}
<?php
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
$drinksServedEvend = new DrinksServed(
$this->getAggregateId(),
$drinksServed
);
$this->recordThat($drinksServedEvend);
$this->apply($drinksServedEvend);
}
}
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
$this->applyAndRecordThat(new DrinksServed(
$this->getAggregateId(),
$drinksServed
));
}
}
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
$this->applyAndRecordThat(new DrinksServed(
$this->getAggregateId(),
$drinksServed
));
}
}
1 - Comprobamos que el evento se puede aplicar
class Tab extends Aggregate {
public function serveDrinks($drinksServed)
{
$this->assertDrinksAreOutstanding($drinksServed);
$this->applyAndRecordThat(new DrinksServed(
$this->getAggregateId(),
$drinksServed
));
}
}
1 - Comprobamos que el evento se puede aplicar2 - Lo Aplicamos y lo guardamos
Repositorio
Recuperar flujo de eventos
Persistir eventos guardados
Publicar el flujo eventos
<?php
interface AggregateRepository
{
public function get(IdentifiesAggregate $aggregateId):
AggregateRoot;
public function add(RecordsEvents $aggregate);
}
<?php
class TabEventSourcingRepository implements TabRepository
{
private $eventStore;
private $projector;
public function __construct(
EventStore $eventStore,
$projector
) {
$this->eventStore = $eventStore;
$this->projector = $projector;
}
}
Event Store
<?php
interface EventStore
{
public function commit(DomainEvents $events);
public function getAggregateHistoryFor(IdentifiesAggregate $id);
}
Serializar
{ "type": "TabOpened", "created_on": 1495579156, "data": { "id" : "8b486a7b-2e32-4e17-ad10-e90841286722", "waiter" : "Jhon Doe", "table" : 1 }}
{ "type": "DrinksOrdered", "created_on": 1495579200, "data": { "id" : "8b486a7b-2e32-4e17-ad10-e90841286722", "items" : [1,2] }}
8b486a7b-2e32-4e17-ad10-e90841286722
Proyecciones
<?php
interface Projection
{
public function eventType();
public function project($event);
}
<?php
class TabOpenedProjection implements Projection
{
private $pdo;
public function __construct($pdo)
{
$this->pdo = $pdo;
}
public function project($event)
{
$stmt = $this->pdo->prepare("INSERT INTO tabs (tab_id, waiter, tableNumber, open) VALUES
(:tab_id, :waiter, :tableNumber, 1)");
$stmt->execute([
':tab_id' => $event->getAggregateId(),
':waiter' => $event->getWaiterId(),
':tableNumber' => $event->getTableNumber(),
]);
}
public function eventType()
{
return TabOpened::class;
}
}
<?php
class Projector
{
private $projections = [];
public function register(array $projections)
{
foreach ($projections as $projection) {
$this->projections[$projection->eventType()] = $projection;
}
}
public function project(DomainEvents $events)
{
foreach ($events as $event) {
if (!isset($this->projections[get_class($event)]))
throw new NoProjectionExists();
$this->projections[get_class($event)]->project($event);
}
}
}
Modelo lectura
Modelo anémico
DTO
Entidades generadas ORM
Repositorios generados ORM
Frameworks
There are no dumb questions ...
https://github.com/malotor/cafe_events
PHP 7.1
Phpunit 6
Docker
☁ events_cafe [master] tree -L 1.├── README.md├── bootstrap.php├── build├── cache├── cli-config.php├── composer.json├── composer.lock├── coverage├── docker-compose.yml├── phpunit.xml├── public├── resources├── scripts├── src├── tests└── vendor
☁ events_cafe [master] tree -L 2 srcsrc├── Application│ ├── Command│ ├── DataTransformer│ └── Query├── Domain│ ├── Model│ └── ReadModel└── Infrastructure ├── CommandBus ├── Persistence ├── Serialize └── ui
☁ events_cafe [master] tree -L 2 src/Applicationsrc/Application├── Command│ ├── CloseTab.php│ ├── CloseTabHandler.php│ ├── MarkDrinksServedCommand.php│ ├── MarkDrinksServedHandler.php│ ├── MarkFoodServedCommand.php│ ├── MarkFoodServedHandler.php│ ├── OpenTabCommand.php│ ├── OpenTabHandler.php│ ├── PlaceOrderCommand.php│ ├── PlaceOrderHandler.php│ ├── PrepareFoodCommand.php│ └── PrepareFoodHandler.php├── DataTransformer│ ├── DataTranformer.php│ └── TabToArrayDataTransformer.php└── Query ├── AllTabsQuery.php ├── AllTabsQueryHandler.php ├── OneTabQuery.php └── OneTabQueryHandler.php
☁ events_cafe [master] tree -L 4 src/Infrastructuresrc/Infrastructure├── CommandBus│ └── CustomInflector.php├── Persistence│ ├── Domain│ │ └── Model│ │ ├── DoctrineOrderedItemRepository.php│ │ ├── InMemoryTabRepository.php│ │ └── TabEventSourcingRepository.php│ ├── EventStore│ │ ├── EventStore.php│ │ ├── PDOEventStore.php│ │ └── RedisEventStore.php│ └── Projection│ ├── BaseProjection.php│ ├── DrinksOrderedProjection.php│ ├── Projection.php│ ├── Projector.php│ ├── TabOpenedProjection.php│ └── TabProjection.php├── Serialize│ ├── JsonSerializer.php│ └── Serializer.php└── ui └── web └── app.php
☁ events_cafe [master] tree -L 2 src/Domainsrc/Domain├── Model│ ├── Aggregate│ ├── Events│ ├── OrderedItem│ └── Tab└── ReadModel ├── Items.php └── Tabs.php
☁ events_cafe [master] tree -L 2 src/Domain/Modelsrc/Domain/Model├── Aggregate│ ├── Aggregate.php│ └── AggregateId.php├── Events│ ├── DrinksOrdered.php│ ├── DrinksServed.php│ ├── FoodOrdered.php│ ├── FoodPrepared.php│ ├── FoodServed.php│ ├── TabClosed.php│ ├── TabEvent.php│ └── TabOpened.php├── OrderedItem│ ├── OrderedItem.php│ ├── OrderedItemNotExists.php│ └── OrderedItemsRepository.php└── Tab ├── DrinkIsNotOutstanding.php ├── FoodIsNotPrepared.php ├── FoodNotOutstanding.php ├── MustPayEnoughException.php ├── Tab.php ├── TabHasUnservedItems.php ├── TabId.php ├── TabNotExists.php ├── TabNotOpenException.php └── TabRepository.php
<?php
$app->post('/tab', function (Request $request) use ($app) {
// …
$command = new Command\OpenTabCommand(
\Ramsey\Uuid\Uuid::uuid4(),
$data['table'],
$data['waiter']
);
$app['command_bus']->handle($command);
// …
})
<?php
$app->get('/tab/{id}', function (Request $request, $id) use ($app)
{
$query = new Query\OneTabQuery($id);
$response = $app['query_bus']->handle($query);
return $app->json([
'tab' => $response
]);
});
Gracias y ..
Que la fuerza os acompañe.