@MarkRedeman
- Event Sourcing (ES)
- Command Query Responsibility Segregation (CQRS)
- Hexagonal architecture
- TestDrivenDevelopment (TDD)
- Domain Driven Design (DDD)
- Behavior Driven Development (BDD)
And lots of acronyms and other cool words [fn:1].
At the Dutch PHP Conference there was
- 1 workshop on the basics of CQRS and Event Sourcing
- 1 keynote about event sourcing
- 4 talks that had something to do with Event Sourcing [fn:2]
- Domain Events as first class citizens
(e.g.
UserWasRegistered,ActivityWasPlanned,MembershipFeeWasPayed) - Persist entities by only storing their domain events
- AuthorWasInvitedByEditor.php
- AuthorWasRegistered.php
- EditorWasRegistered.php
- FormularyChangedName.php
- FormularyCreated.php
- IndicationWasArchived.php
- IndicationWasCreated.php
- IndicationWasPublished.php
- IndicationWasRenamed.php
- IndicationWasRestored.php
- TherapySchemesWereReordered.php
- TherapySchemeWasAddedToIndication.php
- TherapySchemeWasRemovedFromIndication.php
- TherapySchemeWasValidated.php
- GroupWasCreated.php
- GroupWasRenamed.php
- MedicineWasAddedToGroup.php
- MedicineWasCreated.php
- MedicineWasRemovedFromGroup.php
- PreferredMedicineWasChosen.php
- PreferredMedicineWasRemoved.php
- GroupWasAppendedToStep.php
- GroupWasRemovedFromStep.php
- MedicinesAndGroupsWereReordered.php
- StepWasArchived.php
- StepWasCreated.php
- StepWasRenamed.php
- StepWasRestored.php
- UnclassifiedMedicineWasAppendedToStep.php
- UnclassifiedMedicineWasRemoved.php
- StepsWereReordered.php
- StepWasAddedToTherapyScheme.php
- StepWasRemovedFromTherapyScheme.php
- TherapySchemeWasArchived.php
- TherapySchemeWasCreated.php
- TherapySchemeWasMadeInvalid.php
- TherapySchemeWasMadeReady.php
- TherapySchemeWasRenamed.php
- TherapySchemeWasRestored.php
- Tell Don’t Ask
- Loosly coupled code
- No loss of data
- Very testable
- More boilerplate
- More complex?
- Requires you to think before writing code (advantage!?)
- Write event based tests for aggregates & projections
- Use in memory repositories when testing aggregates & read models
- Integration tests for generic repositories
- Write the test
use Broadway\EventSourcing\Testing\AggregateRootScenarioTestCase;
class BuildingTest extends AggregateRootScenarioTestCase
{
/** @test */
function a_user_can_check_in_to_a_building
{
$id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');
$this->scenario
->given([
new BuildingWasRegistered($id)
])
->when(function (Building $building) {
$building->checkInUser(string $username)
})
->then([
new UserCheckedIntoBuilding($id, $username)
]);
}
}- Implement the test
final class Building extends EventSourcedAggregateRoot
{
// ...
public function checkInUser(string $username)
{
$this->apply(
new UserCheckedIntoBuilding($this->id, $username)
);
}
}- Improve the test
use Broadway\EventSourcing\Testing\AggregateRootScenarioTestCase;
class BuildingTest extends AggregateRootScenarioTestCase
{
/** @test */
function a_user_can_check_in_to_a_building
{
$id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');
$this->scenario
->given([
new BuildingWasRegistered($id)
])
->when(function (Building $building) {
$building->checkInUser(string $username)
})
->then([
new UserCheckedIntoBuilding($id, $username)
])
// A user can't check into a building twice
->expectExceptionWhen(
CantCheckIntoBuilding::class,
function (Building $building) {
$building->checkInUser(string $username);
}
);
}
}- Fix the test
final class Building extends EventSourcedAggregateRoot
{
// ...
private $checkedInUsers = [];
public function checkInUser(string $username)
{
// Don't let a user check in more than once
if ($this->userIsCheckedIn($username)) {
throw CantCheckIntoBuilding::user($username);
}
$this->apply(
new UserCheckedIntoBuilding($this->id, $username)
);
}
private function userIsCheckedIn(string $username) : bool
{
return in_array($username, $this->checkedInUsers, true);
}
private function whenUserCheckedIntoBuilding(UserCheckedIntoBuilding $event)
{
$this->checkedInUsers[] = $event->username();
}
}
final class CantCHeckIntoBuilding extends \LogicException
{
public static function user(string $username)
{
return new self(sprintf('User "%s" is not checked in', $username));
}
}The customer wants a view of all checked in users of a specific building.
- Write a read model containing the username data for a building (make it serializable)
class CheckedInUsersTest extends TestCase
{
/** @test */
function it_keeps_track_of_the_users_checked_into_a_building()
{
$id = new BuildingId('943ed5e8-7058-4b2e-90cd-8b131152faa3');
$readModel = CheckedInUsers::forBuilding($id);
$this->assertInstanceOf(CheckedInUsers::class, $readModel);
$this->assertEquals($id, $readModel->buildingId());
}
/** @test */
function a_user_can_be_added_to_the_read_model()
{
$id = new BuildingId('943ed5e8-7058-4b2e-90cd-8b131152faa3');
$readModel = CheckedInUsers::forBuilding($id);
$readModel->addUsername("Mark");
$this->assertEquals(["Mark"], $readModel->usernames());
}
// .. etc.
}final class CheckedInUsers
{
/** @var string[] */
private $usersnames;
/** @var string */
private $buildingId;
public static function forBuilding(BuildingId $id) : CheckedInUsers
{
$model = new self;
$model->buildingId = $id;
return $model;
}
public function addUsername(string $username)
{
$this->usernames[] = $username;
}
public function usernames() : array
{
return $this->usernames;
}
public function buildingId() : BuildingId
{
return new BuildingId($this->buildingId);
}
// some stuff to make it serializable
}- Write a projection that populates the read model
class CheckedInUsersProjectorTest extends ProjectorScenarioTestCase
{
protected function createProjector(InMemoryRepository $repository)
{
return new CheckedInUsersProjector($repository);
}
/** @test */
function it_starts_keeping_track_of_a_buildings_checked_in_users()
{
$id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');
$this->scenario->when(
new BuildingWasRegistered($id)
)->then(function (InMemoryRepository $repo) use ($id) {
// Check that our in memory repository contains the building
$this->assertEquals(
CheckedInUsers::forBuilding($id),
$repo->find($id)
);
}]);
}
/** @test */
function it_keeps_track_of_a_buildings_checked_in_users()
{
$id = new BuildingId('8838aa4b-2fa2-4fdf-b1c0-53a8fb1f2c26');
$this->scenario->when(
new BuildingWasRegistered($id),
new UserCheckedIntoBuilding($id, "Mark"),
new UserCheckedIntoBuilding($id, "Piet"),
new UserCheckedIntoBuilding($id, "Klaas")
)->then(function (InMemoryRepository $repo) use ($id) {
$readModel = $repo->find($id);
$this->assertEquals(
["Mark", "Piet", "Klaas"],
$readModel->usernames()
);
}
);
}
}- Persist the read model
- Improve testing of read models (lots of duplicated code)
Open source event sourced application: Professor Francken
[fn:1] See Matthias Noback’s talk “All the cool kids…”
[fn:2] Talks:
- Integrating Bounded Contexts (Carlos Buenosvinos)
- CQRS and Domain Events for integration (Giorgio Sironi)
- Event Sourcing: the good, the bad and the complicated (Marco Pivetta)
- How I Built A Video Game using Event Sourcing (Shawn McCool)
- Models & Service Layers; Hemoglobin & Hobgoblins (Ross Tuck) https://youtu.be/ajhqScWECMo
- Event Sourcing is actually just functional code» (Greg Young) https://www.youtube.com/watch?v=kZL41SMXWdM