From b8830668fa87a51227d68f3cf29b4d7cd0895f12 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 19 Feb 2021 13:31:05 +0100 Subject: [PATCH 01/16] aggregate docs in german --- docs/aggregate.md | 572 +++++++++++++++++++++++++++++++++++++++++++++ docs/pipeline.md | 1 + docs/processor.md | 1 + docs/projection.md | 1 + docs/repository.md | 1 + docs/snapshots.md | 1 + docs/store.md | 2 + 7 files changed, 579 insertions(+) create mode 100644 docs/aggregate.md create mode 100644 docs/pipeline.md create mode 100644 docs/processor.md create mode 100644 docs/projection.md create mode 100644 docs/repository.md create mode 100644 docs/snapshots.md create mode 100644 docs/store.md diff --git a/docs/aggregate.md b/docs/aggregate.md new file mode 100644 index 000000000..8e40a1652 --- /dev/null +++ b/docs/aggregate.md @@ -0,0 +1,572 @@ +# Aggregate + +TODO: Aggregate Root definition + +Ein AggregateRoot muss von `AggregateRoot` erben und die Methode `aggregateRootId` implementieren. +Später werden noch events hinzugefügt, aber das folgende reicht schon aus, dass es ausführbar ist. + + +```php +profileRepository = $profileRepository; + } + + public function __invoke(CreateProfile $command): void + { + $profile = Profile::create($command->id()); + + $this->profileRepository->save($profile); + } +} +``` + +Wenn man jetzt in der DB schauen würde, würde man sehen, dass nichts gespeichert wurde. +Das liegt daran, dass nur Events in der Datenbank gespeichert werden und solange keine Events existiert, +passiert nichts. + +Info: Ein CommandBus System ist nicht notwendig, nur empfehlenswert. +Die Interaktion kann auch ohne weiteres in einem Controller oder Service passieren. + +## Event + +Informationen werden nur in form von Events gespeichert. +Diese Events werden auch wieder dazu verwendet, um den aktuellen State des Aggregates wieder aufzubauen. + +### create aggregate + +Damit auch wirklich ein Aggregate gespeichert wird, muss mindestens ein Event in der DB existieren. +Ein "Create" Event bietet sich hier an. + +```php + $id, + 'name' => $name + ] + ); + } + + public function profileId(): string + { + return $this->aggregateId; + } + + public function name(): string + { + return $this->payload['name']; + } +} +``` + +Wir empfehlen hier eine named constructor und methoden mit typehints zu definieren, +damit das handling einfach wird und weniger fehleranfällig. + +Ein Event muss den AggregateRoot id übergeben bekommen und den payload. +Der payload muss als json serialisierbar und unserialisierbar sein. +Sprich, es darf nur aus einfachen Datentypen bestehen (keine Objekte). + +Nachdem wir das Event defiert haben, müssen wir das erstellen des Profils anpassen. + +```php +id; + } + + public function name(): string + { + return $this->name; + } + + public static function create(string $id, string $name): self + { + $self = new self(); + $self->apply(ProfileCreated::raise($id, $name)); + + return $self; + } + + protected function applyProfileCreated(ProfileCreated $event): void + { + $this->id = $event->profileId(); + $this->name = $event->name(); + } +} +``` + +Wir haben hier das Event in `create` erzeugt +und dieses Event mit der Methode `apply` gemerkt. + +Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den State anzupassen. +Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, +indem es einfach ein `apply` vorne hinzufügt. + +Vorsicht: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. +Manche Events verändern nicht den State (wenn nicht nötig), +sondern werden ggfs. nur in Projections verwendet. + +Nachdem ein event mit `->apply()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- +Sprich, nach diesem Call ist der State dem entsprechend schon aktuallisiert. + +### modify aggregate + +Um Aggregate nachträglich zu verändern, müssen nur weitere Events definiert werden. +Zb. als können wir den Namen ändern. + +```php + $name + ] + ); + } + + public function profileId(): string + { + return $this->aggregateId; + } + + public function name(): string + { + return $this->payload['name']; + } +} +``` + +Nachdem wir das Event definiert haben, können wir unser Aggregat erweitern. + +```php +id; + } + + public function name(): string + { + return $this->name; + } + + public static function create(string $id, string $name): self + { + $self = new self(); + $self->apply(ProfileCreated::raise($id, $name)); + + return $self; + } + + public function changeName(string $name): void + { + $this->apply(NameChanged::raise($this->id, $name)); + } + + protected function applyProfileCreated(ProfileCreated $event): void + { + $this->id = $event->profileId(); + $this->name = $event->name(); + } + + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name(); + } +} +``` + +Auch hierfür fügen wir eine Methode hinzu, um das Event zu registrieren, +und eine apply Methode, um das ganze auszuführen. + +Das ganze können wir dann wie folgt verwenden. + +```php +profileRepository = $profileRepository; + } + + public function __invoke(ChangeName $command): void + { + $profile = $this->profileRepository->load($command->id()); + $profile->changeName($command->name()); + + $this->profileRepository->save($profile); + } +} +``` + +Hier wird das Aggregat geladen, indem alle Events aus der Datenbank geladen wird. +Diese Events werden dann wieder mit der apply Methoden ausgeführt, um den Aktuellen State aufzubauen. +Das alles passiert in der load Methode automatisch. + +Daraufhin wird `$profile->changeName()` mit dem neuen Namen aufgerufen. +Intern wird das Event `NameChanged` geworfen und als nicht "gespeichertes" Event gemerkt. + +Zum Schluss wird die `save()` Methode aufgerüfen, +die wiederrum alle nicht gespeicherte Events aus dem Aggregate zieht +und diese dann in die Datenbank speichert. + +### business rules + +Business Rules müssen immer in den Methoden passieren, die die Events werfen. +Sprich, in unserem Fall in `create` oder in `changeName` Methoden. + +In den Apply Methoden darf nicht mehr überprüft werden, ob die Aktion Valide ist, +da das Event schon passiert ist. Außerdem können diese Events schon in der Datenbank sein, +und somit würde der State aufbau nicht mehr möglich sein. + +```php +id; + } + + // ... + + public function name(): string + { + return $this->name; + } + + public function changeName(string $name): void + { + if (strlen($name) < 3) { + throw new NameIsToShortException($name); + } + + $this->apply(NameChanged::raise($this->id, $name)); + } + + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name(); + } +} +``` + +Diese Regel mit der länge des Namens ist derzeit nur in changeName definiert. +Damit diese Regel auch beim erstellen greift, muss diese entweder dort auch implementiert werden +oder besser, man erstellt ein Value Object. + +```php +value = $value; + } + + public function toString(): string + { + return $this->value; + } +} +``` + +Den Name Value Object kann man dann wie folgt verwenden. + +```php +id; + } + + public static function create(string $id, Name $name): self + { + $self = new self(); + $self->apply(ProfileCreated::raise($id, $name)); + + return $self; + } + + // ... + + public function name(): Name + { + return $this->name; + } + + public function changeName(Name $name): void + { + $this->apply(NameChanged::raise($this->id, $name)); + } + + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name(); + } +} +``` + +Damit das ganze auch funktioniert, müssen wir unser Event noch anpassen, +damit es als json serialisierbar ist. + +```php + $name->toString() + ] + ); + } + + public function profileId(): string + { + return $this->aggregateId; + } + + public function name(): Name + { + return new Name($this->payload['name']); + } +} +``` + +Es gibt auch die Fälle, dass ein abhängig von States Regeln definiert werden. +Manchmal auch von States, die erst in der Methode zu stande kommen. +Das ist kein Problem, da die apply Methoden immer sofort ausgeführt werden. + +```php +people === self::SIZE) { + throw new NoPlaceException($name); + } + + $this->apply(BookRoom::raise($this->id, $name)); + + if ($this->people === self::SIZE) { + $this->apply(FullyBooked::raise($this->id)); + } + } + + protected function applyBookRoom(BookRoom $event): void + { + $this->people++; + } +} +``` + +In diesem Fall schmeißen wir ein zusätzliches Event, wenn unser Hotel ausgebucht ist, +um weitere Systeme zu informieren, wie zB. unsere Webseite mithilfe von einer Projection +oder ein fremdes System, um keine Buchungen mehr zu erlauben. + +Denkbar wäre auch, dass hier nachträglich eine Exception geschmissen wird. +Da erst bei der save Methode die Events wirklich gespeichert werden, +kann hier ohne weiteres darauf reagiert werden, ohne dass ungewollt Daten verändert werden. + +### override handle methode + +Wenn die standard Implementierung aus gründen nicht reicht oder zum Umständlich ist, +dann kann man diese auch überschreiben. Hier findest du ein kleines Beispiel. + +```php +id = $event->profileId(); + $this->name = $event->name(); + break; + class NameChanged::class: + $this->name = $event->name(); + break; + } + } +} +``` + diff --git a/docs/pipeline.md b/docs/pipeline.md new file mode 100644 index 000000000..454a092fc --- /dev/null +++ b/docs/pipeline.md @@ -0,0 +1 @@ +# Pipeline diff --git a/docs/processor.md b/docs/processor.md new file mode 100644 index 000000000..6a5b45a0f --- /dev/null +++ b/docs/processor.md @@ -0,0 +1 @@ +# Processor diff --git a/docs/projection.md b/docs/projection.md new file mode 100644 index 000000000..218b3311d --- /dev/null +++ b/docs/projection.md @@ -0,0 +1 @@ +# Projections diff --git a/docs/repository.md b/docs/repository.md new file mode 100644 index 000000000..49998d789 --- /dev/null +++ b/docs/repository.md @@ -0,0 +1 @@ +# Repository diff --git a/docs/snapshots.md b/docs/snapshots.md new file mode 100644 index 000000000..dd01b109d --- /dev/null +++ b/docs/snapshots.md @@ -0,0 +1 @@ +# Snapshots diff --git a/docs/store.md b/docs/store.md new file mode 100644 index 000000000..8e703b0bb --- /dev/null +++ b/docs/store.md @@ -0,0 +1,2 @@ +# Store + From 3534b529b72f7c0df4dc0fd06f15dc5e3b76f766 Mon Sep 17 00:00:00 2001 From: David Badura Date: Fri, 19 Feb 2021 14:37:28 +0100 Subject: [PATCH 02/16] some improvemets --- docs/aggregate.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 8e40a1652..aa096ac78 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -29,7 +29,7 @@ final class Profile extends AggregateRoot } ``` -Wir verwenden hier ein sogenanten named constructor, um ein Objekt zu erzeugen. +Wir verwenden hier ein sogenannten named constructor, um ein Objekt zu erzeugen. Der Konstruktor selbst ist protected und kann nicht von Außen aufgerufen. Aber es besteht die Möglichkeit mehrere verschiedene named constructor zu definieren. @@ -78,7 +78,7 @@ Diese Events werden auch wieder dazu verwendet, um den aktuellen State des Aggre ### create aggregate -Damit auch wirklich ein Aggregate gespeichert wird, muss mindestens ein Event in der DB existieren. +Damit auch wirklich ein Aggregat gespeichert wird, muss mindestens ein Event in der DB existieren. Ein "Create" Event bietet sich hier an. ```php @@ -115,14 +115,14 @@ final class ProfileCreated extends AggregateChanged } ``` -Wir empfehlen hier eine named constructor und methoden mit typehints zu definieren, -damit das handling einfach wird und weniger fehleranfällig. +Wir empfehlen hier named constructoren und methoden mit typehints zu verwenden, +damit das handling einfacher wird und weniger fehleranfällig. Ein Event muss den AggregateRoot id übergeben bekommen und den payload. Der payload muss als json serialisierbar und unserialisierbar sein. Sprich, es darf nur aus einfachen Datentypen bestehen (keine Objekte). -Nachdem wir das Event defiert haben, müssen wir das erstellen des Profils anpassen. +Nachdem wir das Event definiert haben, müssen wir das Erstellen des Profils anpassen. ```php apply()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- -Sprich, nach diesem Call ist der State dem entsprechend schon aktuallisiert. +Sprich, nach diesem Call ist der State dem entsprechend schon aktualisiert. ### modify aggregate Um Aggregate nachträglich zu verändern, müssen nur weitere Events definiert werden. -Zb. als können wir den Namen ändern. +Zb. können wir auch den Namen ändern. ```php changeName()` mit dem neuen Namen aufgerufen. Intern wird das Event `NameChanged` geworfen und als nicht "gespeichertes" Event gemerkt. -Zum Schluss wird die `save()` Methode aufgerüfen, +Zum Schluss wird die `save()` Methode aufgerufen, die wiederrum alle nicht gespeicherte Events aus dem Aggregate zieht und diese dann in die Datenbank speichert. @@ -324,7 +324,12 @@ Sprich, in unserem Fall in `create` oder in `changeName` Methoden. In den Apply Methoden darf nicht mehr überprüft werden, ob die Aktion Valide ist, da das Event schon passiert ist. Außerdem können diese Events schon in der Datenbank sein, -und somit würde der State aufbau nicht mehr möglich sein. +und somit würde der State aufbau nicht mehr möglich sein. + +Außerdem dürfen in den Apply Methoden keine weiteren Events geworfen werden, +da diese Methoden immer verwendet werden, um den aktuellen State aufzubauen. +Das hätte sonst die Folge, dass beim Laden immer neue Evens erzeugt werden. +Wie Abhängigkeiten von Events implementiert werden können, steht weiter unten. ```php Date: Mon, 22 Feb 2021 10:48:56 +0100 Subject: [PATCH 03/16] add pipeline doku --- docs/aggregate.md | 4 +- docs/pipeline.md | 245 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 2 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index aa096ac78..190d3e92f 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -317,7 +317,7 @@ Zum Schluss wird die `save()` Methode aufgerufen, die wiederrum alle nicht gespeicherte Events aus dem Aggregate zieht und diese dann in die Datenbank speichert. -### business rules +## business rules Business Rules müssen immer in den Methoden passieren, die die Events werfen. Sprich, in unserem Fall in `create` oder in `changeName` Methoden. @@ -541,7 +541,7 @@ Denkbar wäre auch, dass hier nachträglich eine Exception geschmissen wird. Da erst bei der save Methode die Events wirklich gespeichert werden, kann hier ohne weiteres darauf reagiert werden, ohne dass ungewollt Daten verändert werden. -### override handle methode +## override handle methode Wenn die standard Implementierung aus gründen nicht reicht oder zum Umständlich ist, dann kann man diese auch überschreiben. Hier findest du ein kleines Beispiel. diff --git a/docs/pipeline.md b/docs/pipeline.md index 454a092fc..538014659 100644 --- a/docs/pipeline.md +++ b/docs/pipeline.md @@ -1 +1,246 @@ # Pipeline + +Ein Store ist immutable, sprich es darf nicht mehr nachträglich verändert werde. +Dazu gehört sowohl das Manipulieren von Events als auch das Löschen. + +Stattdessen kann man den Store duplizieren und dabei die Events manipulieren. +Somit bleibt der alte Store unberührt und man kann vorher den neuen Store durchtesten, +ob die Migration funktioniert hat. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Pipeline; +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; + +$pipeline = new Pipeline( + new StoreSource($oldStore), + new StoreTarget($newStore), + [ + new ExcludeEventMiddleware([PrivacyAdded::class]), + new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return NewVisited::raise($oldVisited->profileId()); + }), + new RecalculatePlayheadMiddleware(), + ] +); +``` + +Oder man kann eine oder mehrere Projection neu erstellen, +wenn entweder neue Projection existieren oder bestehende verändert wurden. + +```php +use Patchlevel\EventSourcing\Pipeline\Pipeline; +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; + +$pipeline = new Pipeline( + new StoreSource($store), + new ProjectionTarget($projection) +); +``` + +Das Prinzip bleibt dabei gleich. Es gibt eine Source, woher die Daten kommen. +Ein Target wohin die Daten fließen sollen. +Und beliebig viele Middlewares um mit den Daten vorher irgendetwas anzustellen. + +## Source + +Als Erstes braucht man eine Quelle. Derzeit gibt es nur den `StoreSource` und `InMemorySource` als Quelle. +Ihr könnt aber jederzeit eigene Sources hinzufügen, +indem ihr das Interface `Patchlevel\EventSourcing\Pipeline\Source\Source` implementiert. +Hier könnt ihr zB. auch von anderen Event-Sourcing Systeme migrieren. + +### Store + +Der StoreSource ist die standard Quelle um alle Events aus der Datenbank zu laden. + +```php +use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; + +$source = new StoreSource($store); +``` + +### In Memory + +Den InMemorySource kann dazu verwendet werden, um tests zu schreiben. + +```php +use Patchlevel\EventSourcing\Pipeline\EventBucket; +use Patchlevel\EventSourcing\Pipeline\Source\InMemorySource; + +$source = new InMemorySource([ + new EventBucket( + Profile::class, + ProfileCreated::raise(Email::fromString('d.a.badura@gmail.com'))->recordNow(0), + ), + // ... +]); +``` + +## Target + +Ziele dienen dazu, um die Daten am Ende des Process abzuarbeiten. +Das kann von einem anderen Store bis hin zu Projektionen alles sein. + +### Store + +Als Target kann man einen neuen Store verwendet werden. +Hierbei ist es egal, ob der vorherige Store ein SingleTable oder MultiTable war. +Sprich, man kann auch nachträglich zwischen den beiden Systemen migrieren. + +Wichtig ist aber, dass nicht derselbe Store verwendet wird! +Ein Store ist immutable und darf nur dupliziert werden! + +```php +use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; + +$target = new StoreTarget($store); +``` + +### Projection + +Eine Projection kann man auch als Target verwenden, +um zum beispiel eine neue Projection aufzubauen oder eine Projection neu zu bauen. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; + +$target = new ProjectionTarget($projection); +``` + +### Projection Repository + +Wenn man gleich alle Projections neu bauen oder erzeugen möchte, +dann kann man auch das ProjectionRepositoryTarget verwenden. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\ProjectionRepositoryTarget; + +$target = new ProjectionRepositoryTarget($projectionRepository); +``` + +### In Memory + +Für test zwecke kann man hier auch den InMemoryTarget verwenden. + +```php +use Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget; + +$target = new InMemoryTarget(); + +// run pipeline + +$buckets = $target->buckets(); +``` + +## Middlewares + +Um Events bei dem Prozess zu manipulieren, löschen oder zu erweitern, kann man Middelwares verwenden. +Dabei ist wichtig zu wissen, dass einige Middlewares eine recalculation vom playhead erfordert. +Das ist eine Nummerierung der Events, die aufsteigend sein muss. +Ein dem entsprechenden Hinweis wird bei jedem Middleware mitgeliefert. + +### exclude + +Mit dieser Middleware kann man bestimmte Events ausschließen. + +Wichtig: ein recalculation vom Playhead ist notwendig! + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; + +$middleware = new ExcludeEventMiddleware([EmailChanged::class]); +``` + +### include + + +Mit dieser Middleware kann man nur bestimmte Events erlauben. + +Wichtig: ein recalculation vom Playhead ist notwendig! + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; + +$middleware = new IncludeEventMiddleware([ProfileCreated::class]); +``` + +### filter + +Wenn die standard Filter Möglichkeiten nicht ausreichen, kann man auch einen eigenen Filter schreiben. +Dieser verlangt ein boolean als Rückgabewert. `true` um Events zu erlauben, `false` um diese nicht zu erlauben. + +Wichtig: ein recalculation vom Playhead ist notwendig! + +```php +use Patchlevel\EventSourcing\Aggregate\AggregateChanged; +use Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware; + +$middleware = new FilterEventMiddleware(function (AggregateChanged $event) { + if (!$event instanceof ProfileCreated) { + return true; + } + + return $event->allowNewsletter(); +}); +``` + + +### replace + +Wenn man ein Event ersetzen möchte, kann man den ReplaceEventMiddleware verwenden. +Als ersten Parameter muss man die Klasse definieren, die man ersetzen möchte. +Und als zweiten Parameter ein Callback, +dass den alten Event erwartet und ein neues Event zurückliefert. +Die Middleware übernimmt dabei den playhead und recordedAt informationen. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; + +$middleware = new ReplaceEventMiddleware(OldVisited::class, static function (OldVisited $oldVisited) { + return NewVisited::raise($oldVisited->profileId()); +}); +``` + +### class rename + +Wenn ein Mapping nicht notwendig ist und man nur die Klasse umbenennen möchte +(zB. wenn Namespaces sich geändert haben), dann kann man den ClassRenameMiddleware verwenden. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; + +$middleware = new ClassRenameMiddleware([ + OldVisited::class => NewVisited::class +]); +``` + +### recalculate playhead + +Mit dieser Middleware kann man den Playhead neu berechnen lassen. +Dieser muss zwingend immer aufsteigend sein, damit das System weiter funktioniert. +Man kann diese Middleware als letztes hinzufügen. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; + +$middleware = new RecalculatePlayheadMiddleware(); +``` + +### chain + +Wenn man seine Middlewares Gruppieren möchte, kann man dazu eine oder mehrere ChainMiddlewares verwenden. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; +use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; + +$middleware = new ChainMiddleware([ + new ExcludeEventMiddleware([EmailChanged::class]), + new RecalculatePlayheadMiddleware() +]); +``` From 25ebf49194667e2543fc51b2cf896b47024d716f Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 23 Feb 2021 14:33:15 +0100 Subject: [PATCH 04/16] add examples --- docs/processor.md | 34 ++++++++++++++++++++++++++++++++++ docs/projection.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ docs/repository.md | 40 ++++++++++++++++++++++++++++++++++++++++ docs/snapshots.md | 39 +++++++++++++++++++++++++++++++++++++++ docs/start.md | 35 +++++++++++++++++++++++++++++++++++ docs/store.md | 27 +++++++++++++++++++++++++++ docs/tests.md | 36 ++++++++++++++++++++++++++++++++++++ 7 files changed, 257 insertions(+) create mode 100644 docs/start.md create mode 100644 docs/tests.md diff --git a/docs/processor.md b/docs/processor.md index 6a5b45a0f..a90449090 100644 --- a/docs/processor.md +++ b/docs/processor.md @@ -1 +1,35 @@ # Processor + +```php +mailer = $mailer; + } + + public function __invoke(AggregateChanged $event): void + { + if (!$event instanceof ProfileCreated) { + return; + } + + $this->mailer->send( + $event->email(), + 'Profile created', + '...' + ); + } +} +``` diff --git a/docs/projection.md b/docs/projection.md index 218b3311d..aa1d857a6 100644 --- a/docs/projection.md +++ b/docs/projection.md @@ -1 +1,47 @@ # Projections + +```php +connection = $connection; + } + + /** @return iterable, string> */ + public function handledEvents(): iterable + { + yield ProfileCreated::class => 'applyProfileCreated'; + } + + public function create(): void + { + $this->connection->executeStatement('CREATE TABLE IF NOT EXISTS projection_profile (id VARCHAR PRIMARY KEY);'); + } + + public function drop(): void + { + $this->connection->executeStatement('DROP TABLE IF EXISTS projection_profile;'); + } + + public function applyProfileCreated(ProfileCreated $profileCreated): void + { + $this->connection->executeStatement( + 'INSERT INTO projection_profile (`id`) VALUES(:id);', + ['id' => $profileCreated->profileId()] + ); + } +} +``` diff --git a/docs/repository.md b/docs/repository.md index 49998d789..750da4821 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -1 +1,41 @@ # Repository + + +## Create + +```php +use Patchlevel\EventSourcing\Repository\Repository; + +$repository = new Repository($store, $eventStream, Profile::class); +``` + +## Usage + +```php +$profile = Profile::create('d.a.badura@gmail.com'); + +$repository->save($profile); +``` + +```php +$profile = $repository->load('229286ff-6f95-4df6-bc72-0a239fe7b284'); +``` + +```php +if(!$repository->has('229286ff-6f95-4df6-bc72-0a239fe7b284')) { + // ... +} +``` + +## Snapshots + +```php +use Patchlevel\EventSourcing\Repository\Repository; +use Patchlevel\EventSourcing\Snapshot\Psr16SnapshotStore; + +$snapshotStore = new Psr16SnapshotStore($cache); + +$repository = new Repository($store, $eventStream, Profile::class, $snapshotStore); +``` + +Mehr dazu diff --git a/docs/snapshots.md b/docs/snapshots.md index dd01b109d..b1f2939ae 100644 --- a/docs/snapshots.md +++ b/docs/snapshots.md @@ -1 +1,40 @@ # Snapshots + +```php + $this->id, + ]; + } + + protected static function deserialize(array $payload): self + { + $self = new self(); + $self->id = $payload['id']; + + return $self; + } +} +``` + +```php +use Patchlevel\EventSourcing\Repository\Repository; +use Patchlevel\EventSourcing\Snapshot\Psr16SnapshotStore; + +$snapshotStore = new Psr16SnapshotStore($cache); + +$repository = new Repository($store, $eventStream, Profile::class, $snapshotStore); +``` diff --git a/docs/start.md b/docs/start.md new file mode 100644 index 000000000..e351d35f3 --- /dev/null +++ b/docs/start.md @@ -0,0 +1,35 @@ +# Getting Started + +```php +use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\Projection\DefaultProjectionRepository; +use Patchlevel\EventSourcing\Projection\ProjectionListener; +use Patchlevel\EventSourcing\Repository\Repository; +use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; +use Patchlevel\EventSourcing\Store\SingleTableStore; + +$profileProjection = new ProfileProjection($this->connection); +$projectionRepository = new DefaultProjectionRepository( + [$profileProjection] +); + +$eventStream = new DefaultEventBus(); +$eventStream->addListener(new ProjectionListener($projectionRepository)); +$eventStream->addListener(new SendEmailProcessor()); + +$store = new SingleTableStore( + $this->connection, + [Profile::class => 'profile'], + 'eventstore' +); + +$repository = new Repository($store, $eventStream, Profile::class); + +// create tables +$profileProjection->create(); + +(new DoctrineSchemaManager())->create($store); + +$profile = Profile::create('1'); +$repository->save($profile); +``` diff --git a/docs/store.md b/docs/store.md index 8e703b0bb..483ed42fd 100644 --- a/docs/store.md +++ b/docs/store.md @@ -1,2 +1,29 @@ # Store +## Single Table Store + +```php +use Patchlevel\EventSourcing\Store\SingleTableStore; + +$store = new SingleTableStore( + $this->connection, + [ + Profile::class => 'profile' + ], + 'eventstore' +); +``` + +## Multi Table Store + +```php +use Patchlevel\EventSourcing\Store\MultiTableStore; + +$store = new MultiTableStore( + $this->connection, + [ + Profile::class => 'profile' + ], + 'eventstore' +); +``` diff --git a/docs/tests.md b/docs/tests.md new file mode 100644 index 000000000..c333847ef --- /dev/null +++ b/docs/tests.md @@ -0,0 +1,36 @@ +# Tests + +```php +releaseEvents(); + + self::assertCount(1, $events); + self::assertInstanceOf(ProfileCreated::class, $events[0]); + self::assertEquals('foo@email.com', $profile->email()->toString()); + } + + public function testRebuild(): void + { + $id = ProfileId::generate(); + + $events = [ + ProfileCreated::raise($id, Email::fromString('foo@email.com')), + ]; + + $profile = Profile::createFromEventStream($events); + + self::assertEquals('foo@email.com', $profile->email()->toString()); + } +} +``` From c0898438266ffacee4a9e3184637d72cba39d0fd Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 30 Nov 2021 17:32:25 +0100 Subject: [PATCH 05/16] update documentation --- docs/aggregate.md | 26 +++++++++++++------------- docs/projection.md | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 190d3e92f..017c47912 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -152,7 +152,7 @@ final class Profile extends AggregateRoot public static function create(string $id, string $name): self { $self = new self(); - $self->apply(ProfileCreated::raise($id, $name)); + $self->record(ProfileCreated::raise($id, $name)); return $self; } @@ -166,17 +166,17 @@ final class Profile extends AggregateRoot ``` Wir haben hier das Event in `create` erzeugt -und dieses Event mit der Methode `apply` gemerkt. +und dieses Event mit der Methode `record` gemerkt. Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den State anzupassen. Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, -indem es einfach ein `apply` vorne hinzufügt. +indem ein `apply` vorne hinzufügt. Vorsicht: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. Manche Events verändern nicht den State (wenn nicht nötig), sondern werden ggfs. nur in Projections verwendet. -Nachdem ein event mit `->apply()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- +Nachdem ein event mit `->record()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- Sprich, nach diesem Call ist der State dem entsprechend schon aktualisiert. ### modify aggregate @@ -248,14 +248,14 @@ final class Profile extends AggregateRoot public static function create(string $id, string $name): self { $self = new self(); - $self->apply(ProfileCreated::raise($id, $name)); + $self->record(ProfileCreated::raise($id, $name)); return $self; } public function changeName(string $name): void { - $this->apply(NameChanged::raise($this->id, $name)); + $this->record(NameChanged::raise($this->id, $name)); } protected function applyProfileCreated(ProfileCreated $event): void @@ -365,7 +365,7 @@ final class Profile extends AggregateRoot throw new NameIsToShortException($name); } - $this->apply(NameChanged::raise($this->id, $name)); + $this->record(NameChanged::raise($this->id, $name)); } protected function applyNameChanged(NameChanged $event): void @@ -432,7 +432,7 @@ final class Profile extends AggregateRoot public static function create(string $id, Name $name): self { $self = new self(); - $self->apply(ProfileCreated::raise($id, $name)); + $self->record(ProfileCreated::raise($id, $name)); return $self; } @@ -446,7 +446,7 @@ final class Profile extends AggregateRoot public function changeName(Name $name): void { - $this->apply(NameChanged::raise($this->id, $name)); + $this->record(NameChanged::raise($this->id, $name)); } protected function applyNameChanged(NameChanged $event): void @@ -519,10 +519,10 @@ final class Hotel extends AggregateRoot throw new NoPlaceException($name); } - $this->apply(BookRoom::raise($this->id, $name)); + $this->record(BookRoom::raise($this->id, $name)); if ($this->people === self::SIZE) { - $this->apply(FullyBooked::raise($this->id)); + $this->record(FullyBooked::raise($this->id)); } } @@ -541,7 +541,7 @@ Denkbar wäre auch, dass hier nachträglich eine Exception geschmissen wird. Da erst bei der save Methode die Events wirklich gespeichert werden, kann hier ohne weiteres darauf reagiert werden, ohne dass ungewollt Daten verändert werden. -## override handle methode +## override apply methode Wenn die standard Implementierung aus gründen nicht reicht oder zum Umständlich ist, dann kann man diese auch überschreiben. Hier findest du ein kleines Beispiel. @@ -560,7 +560,7 @@ use Patchlevel\EventSourcing\Aggregate\AggregateRoot; final class Profile extends AggregateRoot { //... - protected function handle(AggregateChanged $event): void + protected function apply(AggregateChanged $event): void { switch ($event::class) { case ProfileCreated::class: diff --git a/docs/projection.md b/docs/projection.md index aa1d857a6..3999efc39 100644 --- a/docs/projection.md +++ b/docs/projection.md @@ -23,7 +23,7 @@ final class ProfileProjection implements Projection /** @return iterable, string> */ public function handledEvents(): iterable { - yield ProfileCreated::class => 'applyProfileCreated'; + yield ProfileCreated::class => 'handleProfileCreated'; } public function create(): void @@ -36,7 +36,7 @@ final class ProfileProjection implements Projection $this->connection->executeStatement('DROP TABLE IF EXISTS projection_profile;'); } - public function applyProfileCreated(ProfileCreated $profileCreated): void + public function handleProfileCreated(ProfileCreated $profileCreated): void { $this->connection->executeStatement( 'INSERT INTO projection_profile (`id`) VALUES(:id);', From 1c5772029a615f6c484c8f6c67e3098e09686e3d Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 1 Dec 2021 11:37:00 +0100 Subject: [PATCH 06/16] first translations --- docs/aggregate.md | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 017c47912..e19f3c898 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -2,9 +2,8 @@ TODO: Aggregate Root definition -Ein AggregateRoot muss von `AggregateRoot` erben und die Methode `aggregateRootId` implementieren. -Später werden noch events hinzugefügt, aber das folgende reicht schon aus, dass es ausführbar ist. - +An AggregateRoot has to inherit from `AggregateRoot` and implement the method` aggregateRootId`. +Events will be added later, but the following is enough to make it executable: ```php Date: Sat, 11 Dec 2021 15:38:43 +0000 Subject: [PATCH 07/16] update documentation --- docs/aggregate.md | 226 +++++++++++++++++++++++++++++++++------------- 1 file changed, 165 insertions(+), 61 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index e19f3c898..71424789c 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -2,8 +2,10 @@ TODO: Aggregate Root definition -An AggregateRoot has to inherit from `AggregateRoot` and implement the method` aggregateRootId`. -Events will be added later, but the following is enough to make it executable: +An AggregateRoot has to inherit from `AggregateRoot` and implement the methods `aggregateRootId` and `apply`. +`aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity. +And the `apply` method is needed to make the changes described as events. +The events will be added later, but the following is enough to make it executable: ```php $id, @@ -133,6 +140,7 @@ declare(strict_types=1); namespace App\Profile; use App\Profile\Event\ProfileCreated; +use Patchlevel\EventSourcing\Aggregate\AggregateChanged; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; final class Profile extends AggregateRoot @@ -157,11 +165,13 @@ final class Profile extends AggregateRoot return $self; } - - protected function applyProfileCreated(ProfileCreated $event): void + + protected function apply(AggregateChanged $event): void { - $this->id = $event->profileId(); - $this->name = $event->name(); + if ($event instanceof ProfileCreated) { + $this->id = $event->profileId(); + $this->name = $event->name(); + } } } ``` @@ -182,8 +192,8 @@ Sprich, nach diesem Call ist der State dem entsprechend schon aktualisiert. ### modify aggregate -Um Aggregate nachträglich zu verändern, müssen nur weitere Events definiert werden. -Zb. können wir auch den Namen ändern. +In order to change the state of the aggregates afterwards, only further events have to be defined. +As example we can add a `NameChanged` event: ```php $name @@ -218,7 +228,7 @@ final class NameChanged extends AggregateChanged } ``` -Nachdem wir das Event definiert haben, können wir unser Aggregat erweitern. +After we have defined the event, we can expand our aggregate: ```php record(NameChanged::raise($this->id, $name)); } - - protected function applyProfileCreated(ProfileCreated $event): void - { - $this->id = $event->profileId(); - $this->name = $event->name(); - } - protected function applyNameChanged(NameChanged $event): void + protected function apply(AggregateChanged $event): void { - $this->name = $event->name(); + if ($event instanceof ProfileCreated) { + $this->id = $event->profileId(); + $this->name = $event->name(); + } + + if ($event instanceof NameChanged) { + $this->name = $event->name(); + } } } ``` @@ -288,7 +300,7 @@ use App\Profile\Command\ChangeName; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; use Patchlevel\EventSourcing\Repository\Repository; -final class ChangeNameHandler extends AggregateRoot +final class ChangeNameHandler { private Repository $profileRepository; @@ -318,6 +330,124 @@ Zum Schluss wird die `save()` Methode aufgerufen, die wiederrum alle nicht gespeicherte Events aus dem Aggregate zieht und diese dann in die Datenbank speichert. +## apply methods + +### strict apply method + +```php +id; + } + + public function name(): string + { + return $this->name; + } + + public static function create(string $id, string $name): self + { + $self = new self(); + $self->record(ProfileCreated::raise($id, $name)); + + return $self; + } + + public function changeName(string $name): void + { + $this->record(NameChanged::raise($this->id, $name)); + } + + protected function applyProfileCreated(ProfileCreated $event): void + { + $this->id = $event->profileId(); + $this->name = $event->name(); + } + + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name(); + } +} +``` + +### non strict apply method + +```php +id; + } + + public function name(): string + { + return $this->name; + } + + public static function create(string $id, string $name): self + { + $self = new self(); + $self->record(ProfileCreated::raise($id, $name)); + + return $self; + } + + public function changeName(string $name): void + { + $this->record(NameChanged::raise($this->id, $name)); + } + + protected function applyProfileCreated(ProfileCreated $event): void + { + $this->id = $event->profileId(); + $this->name = $event->name(); + } + + protected function applyNameChanged(NameChanged $event): void + { + $this->name = $event->name(); + } +} +``` + ## business rules Business Rules müssen immer in den Methoden passieren, die die Events werfen. @@ -342,9 +472,12 @@ namespace App\Profile; use App\Profile\Event\ProfileCreated; use App\Profile\Event\NameChanged; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\StrictApplyMethod; final class Profile extends AggregateRoot { + use StrictApplyMethod; + private string $id; private string $name; @@ -419,9 +552,12 @@ namespace App\Profile; use App\Profile\Event\ProfileCreated; use App\Profile\Event\NameChanged; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\StrictApplyMethod; final class Profile extends AggregateRoot { + use StrictApplyMethod; + private string $id; private Name $name; @@ -473,7 +609,7 @@ final class NameChanged extends AggregateChanged { public static function raise(string $id, Name $name): AggregateChanged { - return self::occur( + return new self( $id, [ 'name' => $name->toString() @@ -505,9 +641,12 @@ declare(strict_types=1); namespace App\Hotel; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\StrictApplyMethod; final class Hotel extends AggregateRoot { + use StrictApplyMethod; + private const SIZE = 5; private int $people; @@ -541,38 +680,3 @@ oder ein fremdes System, um keine Buchungen mehr zu erlauben. Denkbar wäre auch, dass hier nachträglich eine Exception geschmissen wird. Da erst bei der save Methode die Events wirklich gespeichert werden, kann hier ohne weiteres darauf reagiert werden, ohne dass ungewollt Daten verändert werden. - -## override apply methode - -Wenn die standard Implementierung aus gründen nicht reicht oder zum Umständlich ist, -dann kann man diese auch überschreiben. Hier findest du ein kleines Beispiel. - -```php -id = $event->profileId(); - $this->name = $event->name(); - break; - class NameChanged::class: - $this->name = $event->name(); - break; - } - } -} -``` - From af1387d4634e375a71780a25c605721a00db33c7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 16:45:01 +0000 Subject: [PATCH 08/16] update readme --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++---- docs/aggregate.md | 39 ++++++++++++++++------------ docs/pipeline.md | 22 ++++++++++++---- docs/projection.md | 2 +- docs/repository.md | 2 +- docs/snapshots.md | 30 +++++++++++++++++++++- docs/start.md | 35 ------------------------- 7 files changed, 130 insertions(+), 64 deletions(-) delete mode 100644 docs/start.md diff --git a/README.md b/README.md index d73cb69e0..7113f45bf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,24 @@ Small lightweight event-sourcing library. composer require patchlevel/event-sourcing ``` -## define aggregates +## integration + +* [Symfony](https://github.com/patchlevel/event-sourcing-bundle) +* [Psalm](https://github.com/patchlevel/event-sourcing-psalm-plugin) + +## documentation + +* [Aggregate](docs/aggregate.md) +* [Repository](docs/repository.md) +* [Processor](docs/processor.md) +* [Projection](docs/projection.md) +* [Snapshots](docs/snapshots.md) +* [Pipeline](docs/pipeline.md) +* [Tests](docs/tests.md) + +## Getting Started + +### define aggregates ```php $message->createdAt()->format(DATE_ATOM), ]); } + + public function create(): void + { + // do nothing (collection will be created lazy automatically) + } public function drop(): void { @@ -171,7 +193,39 @@ final class MessageProjection implements Projection } ``` -## usage +### configuration + +```php +use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\Projection\DefaultProjectionRepository; +use Patchlevel\EventSourcing\Projection\ProjectionListener; +use Patchlevel\EventSourcing\Repository\Repository; +use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; +use Patchlevel\EventSourcing\Store\SingleTableStore; + +$messageProjection = new MessageProjection($this->connection); +$projectionRepository = new DefaultProjectionRepository( + [$messageProjection] +); + +$eventStream = new DefaultEventBus(); +$eventStream->addListener(new ProjectionListener($projectionRepository)); +$eventStream->addListener(new SendEmailProcessor()); + +$store = new SingleTableStore( + $this->connection, + [Profile::class => 'profile'], + 'eventstore' +); + +$repository = new Repository($store, $eventStream, Profile::class); + +// create tables +$profileProjection->create(); +(new DoctrineSchemaManager())->create($store); +``` + +### usage ```php profileRepository->store($profile); } } -``` +``` \ No newline at end of file diff --git a/docs/aggregate.md b/docs/aggregate.md index 71424789c..736a7e4b8 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -19,19 +19,24 @@ use Patchlevel\EventSourcing\Aggregate\AggregateRoot; final class Profile extends AggregateRoot { + private string $id; + public function aggregateRootId(): string { - return '1'; + return $this->id; } public static function create(string $id): self { - // todo + $self = new self(); + // todo: record create event + + return $self; } public function apply(AggregateChanged $event): void { - // todo + // todo: apply create event and set id } } ``` @@ -70,7 +75,7 @@ final class CreateProfileHandler } ``` -If you look in the DB now, you would see that nothing has been saved. +Caution: If you look in the DB now, you would see that nothing has been saved. This is because only events are stored in the database and as long as no events exist, nothing happens. @@ -81,13 +86,13 @@ Info: You can find more about repositories in the chapter `Repository`. ## Event -Information is only stored as events. +Aggregate state is only stored as events. These events are also used again to rebuild the current state of the aggregate. ### create aggregate In order that an aggregate is actually saved, at least one event must exist in the DB. -A "Create" event is ideal here: +A `ProfileCreated` event is ideal here: ```php $id, 'name' => $name ] - ); + ); } public function profileId(): string @@ -126,7 +131,7 @@ final class ProfileCreated extends AggregateChanged We recommend using named constructors and methods with typehints, so that handling becomes easier and less error-prone. -An event must receive the AggregateRoot id and the payload. +An event must receive the `aggregateId` and the `payload`. The payload must be serializable and non-serializable as json. In other words, it can only consist of simple data types (no objects). @@ -176,16 +181,10 @@ final class Profile extends AggregateRoot } ``` -Wir haben hier das Event in `create` erzeugt -und dieses Event mit der Methode `record` gemerkt. -Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den State anzupassen. -Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, -indem ein `apply` vorne hinzufügt. -Vorsicht: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. -Manche Events verändern nicht den State (wenn nicht nötig), -sondern werden ggfs. nur in Projections verwendet. +Wir haben hier das Event in `create` erzeugt +und dieses Event mit der Methode `record` gemerkt. Nachdem ein event mit `->record()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- Sprich, nach diesem Call ist der State dem entsprechend schon aktualisiert. @@ -392,6 +391,14 @@ final class Profile extends AggregateRoot ### non strict apply method +Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den State anzupassen. +Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, +indem ein `apply` vorne hinzufügt. + +Vorsicht: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. +Manche Events verändern nicht den State (wenn nicht nötig), +sondern werden ggfs. nur in Projections verwendet. + ```php recordNow(0), + ProfileCreated::raise(Email::fromString('david.badura@patchlevel.de'))->recordNow(0), ), // ... ]); @@ -82,7 +83,7 @@ $source = new InMemorySource([ ## Target -Ziele dienen dazu, um die Daten am Ende des Process abzuarbeiten. +"Ziele" dienen dazu, um die Daten am Ende des Process abzuarbeiten. Das kann von einem anderen Store bis hin zu Projektionen alles sein. ### Store @@ -218,6 +219,17 @@ $middleware = new ClassRenameMiddleware([ ]); ``` +### until + +Ein usecase könnte auch sein, dass man sich die Projektion aus einem vergangenen Zeitpunkt anschauen möchte. +Dabei kann man den UntilEventMiddleware verwenden um nur Events zu erlauben, die vor diesem Zeitpunkt recorded wurden. + +```php +use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; + +$middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:00')); +``` + ### recalculate playhead Mit dieser Middleware kann man den Playhead neu berechnen lassen. diff --git a/docs/projection.md b/docs/projection.md index 3999efc39..06a5b1b5e 100644 --- a/docs/projection.md +++ b/docs/projection.md @@ -5,7 +5,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Projection; +namespace App\Projection; use Doctrine\DBAL\Connection; use Patchlevel\EventSourcing\Aggregate\AggregateChanged; diff --git a/docs/repository.md b/docs/repository.md index 750da4821..0790a3c75 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -12,7 +12,7 @@ $repository = new Repository($store, $eventStream, Profile::class); ## Usage ```php -$profile = Profile::create('d.a.badura@gmail.com'); +$profile = Profile::create('david.badura@patchlevel.de'); $repository->save($profile); ``` diff --git a/docs/snapshots.md b/docs/snapshots.md index b1f2939ae..7d8767373 100644 --- a/docs/snapshots.md +++ b/docs/snapshots.md @@ -5,7 +5,7 @@ declare(strict_types=1); -namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Aggregate; +namespace App\Profile; use Patchlevel\EventSourcing\Aggregate\SnapshotableAggregateRoot; @@ -30,6 +30,7 @@ final class Profile extends SnapshotableAggregateRoot } ``` + ```php use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Snapshot\Psr16SnapshotStore; @@ -38,3 +39,30 @@ $snapshotStore = new Psr16SnapshotStore($cache); $repository = new Repository($store, $eventStream, Profile::class, $snapshotStore); ``` + + +## stores + +### psr16 + +```php +use Patchlevel\EventSourcing\Snapshot\Psr16SnapshotStore; + +$snapshotStore = new Psr16SnapshotStore($cache); +``` + +### psr6 + +```php +use Patchlevel\EventSourcing\Snapshot\Psr6SnapshotStore; + +$snapshotStore = new Psr6SnapshotStore($cache); +``` + +### in memory + +```php +use Patchlevel\EventSourcing\Snapshot\InMemorySnapshotStore; + +$snapshotStore = new InMemorySnapshotStore(); +``` \ No newline at end of file diff --git a/docs/start.md b/docs/start.md deleted file mode 100644 index e351d35f3..000000000 --- a/docs/start.md +++ /dev/null @@ -1,35 +0,0 @@ -# Getting Started - -```php -use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -use Patchlevel\EventSourcing\Projection\DefaultProjectionRepository; -use Patchlevel\EventSourcing\Projection\ProjectionListener; -use Patchlevel\EventSourcing\Repository\Repository; -use Patchlevel\EventSourcing\Schema\DoctrineSchemaManager; -use Patchlevel\EventSourcing\Store\SingleTableStore; - -$profileProjection = new ProfileProjection($this->connection); -$projectionRepository = new DefaultProjectionRepository( - [$profileProjection] -); - -$eventStream = new DefaultEventBus(); -$eventStream->addListener(new ProjectionListener($projectionRepository)); -$eventStream->addListener(new SendEmailProcessor()); - -$store = new SingleTableStore( - $this->connection, - [Profile::class => 'profile'], - 'eventstore' -); - -$repository = new Repository($store, $eventStream, Profile::class); - -// create tables -$profileProjection->create(); - -(new DoctrineSchemaManager())->create($store); - -$profile = Profile::create('1'); -$repository->save($profile); -``` From 9df768db73c535c712aa86536b977be631c608f0 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 16:53:10 +0000 Subject: [PATCH 09/16] test info box --- docs/aggregate.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 736a7e4b8..c02001c4d 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -75,9 +75,9 @@ final class CreateProfileHandler } ``` -Caution: If you look in the DB now, you would see that nothing has been saved. -This is because only events are stored in the database and as long as no events exist, -nothing happens. +> :warning: If you look in the DB now, you would see that nothing has been saved. +> This is because only events are stored in the database and as long as no events exist, +> nothing happens. Info: A CommandBus system is not necessary, only recommended. The interaction can also easily take place in a controller or service. From 7d0c59465fe880f846798708103967504cc33cc7 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 16:58:01 +0000 Subject: [PATCH 10/16] more boxes --- docs/aggregate.md | 14 +++++--------- docs/pipeline.md | 6 +++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index c02001c4d..490d8dd82 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -79,10 +79,8 @@ final class CreateProfileHandler > This is because only events are stored in the database and as long as no events exist, > nothing happens. -Info: A CommandBus system is not necessary, only recommended. -The interaction can also easily take place in a controller or service. - -Info: You can find more about repositories in the chapter `Repository`. +> :book: A CommandBus system is not necessary, only recommended. +> The interaction can also easily take place in a controller or service. ## Event @@ -181,8 +179,6 @@ final class Profile extends AggregateRoot } ``` - - Wir haben hier das Event in `create` erzeugt und dieses Event mit der Methode `record` gemerkt. @@ -395,9 +391,9 @@ Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den St Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, indem ein `apply` vorne hinzufügt. -Vorsicht: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. -Manche Events verändern nicht den State (wenn nicht nötig), -sondern werden ggfs. nur in Projections verwendet. +> :warning: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. +> Manche Events verändern nicht den State (wenn nicht nötig), +> sondern werden ggfs. nur in Projections verwendet. ```php :warning: ein recalculation vom Playhead ist notwendig! ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; @@ -161,7 +161,7 @@ $middleware = new ExcludeEventMiddleware([EmailChanged::class]); Mit dieser Middleware kann man nur bestimmte Events erlauben. -Wichtig: ein recalculation vom Playhead ist notwendig! +> :warning: ein recalculation vom Playhead ist notwendig! ```php use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; @@ -174,7 +174,7 @@ $middleware = new IncludeEventMiddleware([ProfileCreated::class]); Wenn die standard Filter Möglichkeiten nicht ausreichen, kann man auch einen eigenen Filter schreiben. Dieser verlangt ein boolean als Rückgabewert. `true` um Events zu erlauben, `false` um diese nicht zu erlauben. -Wichtig: ein recalculation vom Playhead ist notwendig! +> :warning: ein recalculation vom Playhead ist notwendig! ```php use Patchlevel\EventSourcing\Aggregate\AggregateChanged; From 13c2775a354045c10be59ecd842154fc0339e71c Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 17:03:25 +0000 Subject: [PATCH 11/16] improve readable --- docs/aggregate.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 490d8dd82..1e18e33ea 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -90,6 +90,7 @@ These events are also used again to rebuild the current state of the aggregate. ### create aggregate In order that an aggregate is actually saved, at least one event must exist in the DB. +An event must receive the `aggregateId` and the `payload` and has to inherit from `AggregateChanged`. A `ProfileCreated` event is ideal here: ```php @@ -125,13 +126,11 @@ final class ProfileCreated extends AggregateChanged } } ``` +> :warning: The payload must be serializable and non-serializable as json. +> In other words, it can only consist of simple data types (no objects). -We recommend using named constructors and methods with typehints, -so that handling becomes easier and less error-prone. - -An event must receive the `aggregateId` and the `payload`. -The payload must be serializable and non-serializable as json. -In other words, it can only consist of simple data types (no objects). +> :book: We recommend using named constructors and methods with typehints, +> so that handling becomes easier and less error-prone. After we have defined the event, we have to adapt the creation of the profile: From c986ee827ed85e0539ef9c8e78835ab109b7980c Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 17:16:45 +0000 Subject: [PATCH 12/16] add definition --- docs/aggregate.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 1e18e33ea..ba7350864 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -1,6 +1,8 @@ # Aggregate -TODO: Aggregate Root definition +> Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects +> that can be treated as a single unit. [...] +[DDD Aggregate - Martin Flower](https://martinflower.com/bliki/DDD_Aggregate.html) An AggregateRoot has to inherit from `AggregateRoot` and implement the methods `aggregateRootId` and `apply`. `aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity. @@ -41,6 +43,8 @@ final class Profile extends AggregateRoot } ``` +> :warning: The aggregate is not yet finished and has only been built to the point that you can instantiate the object. + We use a so-called named constructor here to create an object of the AggregateRoot. The constructor itself is protected and cannot be called from outside. But it is possible to define different named constructors for different use-cases like `createFromRegistration`. @@ -126,7 +130,7 @@ final class ProfileCreated extends AggregateChanged } } ``` -> :warning: The payload must be serializable and non-serializable as json. +> :warning: The payload must be serializable and unserializable as json. > In other words, it can only consist of simple data types (no objects). > :book: We recommend using named constructors and methods with typehints, From cf3ecf72c35c649401e8c06e72d0663164637bfd Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 19:32:31 +0100 Subject: [PATCH 13/16] finish aggregate docs --- docs/aggregate.md | 201 ++++++++++++++++++++-------------------------- 1 file changed, 88 insertions(+), 113 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index ba7350864..a8ffa0155 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -2,7 +2,8 @@ > Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects > that can be treated as a single unit. [...] -[DDD Aggregate - Martin Flower](https://martinflower.com/bliki/DDD_Aggregate.html) +> +> :book: [DDD Aggregate - Martin Flower](https://martinflower.com/bliki/DDD_Aggregate.html) An AggregateRoot has to inherit from `AggregateRoot` and implement the methods `aggregateRootId` and `apply`. `aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity. @@ -79,11 +80,11 @@ final class CreateProfileHandler } ``` -> :warning: If you look in the DB now, you would see that nothing has been saved. +> :warning: If you look in the database now, you would see that nothing has been saved. > This is because only events are stored in the database and as long as no events exist, > nothing happens. -> :book: A CommandBus system is not necessary, only recommended. +> :book: A **command bus** system is not necessary, only recommended. > The interaction can also easily take place in a controller or service. ## Event @@ -91,7 +92,7 @@ final class CreateProfileHandler Aggregate state is only stored as events. These events are also used again to rebuild the current state of the aggregate. -### create aggregate +### Create a new aggregate In order that an aggregate is actually saved, at least one event must exist in the DB. An event must receive the `aggregateId` and the `payload` and has to inherit from `AggregateChanged`. @@ -182,13 +183,14 @@ final class Profile extends AggregateRoot } ``` -Wir haben hier das Event in `create` erzeugt -und dieses Event mit der Methode `record` gemerkt. +In our named constructor `create` we have now created the event and recorded it with the method` record`. +The aggregate remembers all recorded events in order to save them later. +At the same time, the `apply` method is executed directly so that we can change our state. -Nachdem ein event mit `->record()` registriert wurde, wird sofort die dazugehörige apply Methode ausgeführt- -Sprich, nach diesem Call ist der State dem entsprechend schon aktualisiert. +In the `apply` method we check what kind of event we have +and then change the `profile` properties `id` and `name` with the transferred values. -### modify aggregate +### Modify an aggregate In order to change the state of the aggregates afterwards, only further events have to be defined. As example we can add a `NameChanged` event: @@ -226,7 +228,10 @@ final class NameChanged extends AggregateChanged } ``` -After we have defined the event, we can expand our aggregate: +> :book: Events should best be written in the past, as they describe a state that has happened. + +After we have defined the event, we can define a new public method called `changeName` to change the profile name. +This method then creates the event `NameChanged` and records it: ```php changeName()` mit dem neuen Namen aufgerufen. -Intern wird das Event `NameChanged` geworfen und als nicht "gespeichertes" Event gemerkt. +To make things structured, you can use two different traits +that allow you to define different apply methods for each event. -Zum Schluss wird die `save()` Methode aufgerufen, -die wiederrum alle nicht gespeicherte Events aus dem Aggregate zieht -und diese dann in die Datenbank speichert. +### Strict apply method -## apply methods +The trait implements the apply method for you. +It is looking for a suitable method for the event by using the short name of the event class +and prefixing it with an `apply`. -### strict apply method +Let's assume that the `App\Profile\Event\NameChanged` event is recorded. +The short class name in this case would be `NameChanged`. +If we prefix the whole thing with `apply`, we get the method` applyNameChanged`. + +This method is then called automatically and the event is passed. +If the method does not exist then an error is thrown. + +Here are two examples with the events `ProfileCreated` and `NameChanged`: ```php id; - } - - public function name(): string - { - return $this->name; - } - - public static function create(string $id, string $name): self - { - $self = new self(); - $self->record(ProfileCreated::raise($id, $name)); + // ... - return $self; - } - - public function changeName(string $name): void - { - $this->record(NameChanged::raise($this->id, $name)); - } - protected function applyProfileCreated(ProfileCreated $event): void { $this->id = $event->profileId(); @@ -388,15 +391,10 @@ final class Profile extends AggregateRoot } ``` -### non strict apply method - -Des Weiteren haben wir eine `applyProfileCreated` Methode, die dazu dient den State anzupassen. -Das AggregateRoot sucht sich mithilfe des Event Short Names `ProfileCreated` die richtige Methode, -indem ein `apply` vorne hinzufügt. +### Non strict apply method -> :warning: Wenn so eine Methode nicht existiert wird das verarbeiten übersprungen. -> Manche Events verändern nicht den State (wenn nicht nötig), -> sondern werden ggfs. nur in Projections verwendet. +The non-strict variant works in the same way as the strict one. +The only difference is that events without a suitable method do not lead to errors. ```php id; - } - - public function name(): string - { - return $this->name; - } - - public static function create(string $id, string $name): self - { - $self = new self(); - $self->record(ProfileCreated::raise($id, $name)); - - return $self; - } - - public function changeName(string $name): void - { - $this->record(NameChanged::raise($this->id, $name)); - } + // ... protected function applyProfileCreated(ProfileCreated $event): void { @@ -454,19 +431,22 @@ final class Profile extends AggregateRoot } ``` -## business rules +> :warning: This can quickly lead to errors, since events are not executed on the aggregate +> and are very difficult to find. We recommend the strict variant. + +## Business rules + +Usually, aggregates have business rules that must be observed. Like there may not be more than 10 people in a group. + +These rules must be checked before an event is recorded. +As soon as an event was recorded, the described thing happened and cannot be undone. -Business Rules müssen immer in den Methoden passieren, die die Events werfen. -Sprich, in unserem Fall in `create` oder in `changeName` Methoden. +A further check in the apply method is also not possible because these events have already happened +and were then also saved in the database. -In den Apply Methoden darf nicht mehr überprüft werden, ob die Aktion Valide ist, -da das Event schon passiert ist. Außerdem können diese Events schon in der Datenbank sein, -und somit würde der State aufbau nicht mehr möglich sein. +> :warning: Disregarding this can break the rebuilding of the state. -Außerdem dürfen in den Apply Methoden keine weiteren Events geworfen werden, -da diese Methoden immer verwendet werden, um den aktuellen State aufzubauen. -Das hätte sonst die Folge, dass beim Laden immer neue Evens erzeugt werden. -Wie Abhängigkeiten von Events implementiert werden können, steht weiter unten. +In the next example we want to make sure that **the name is at least 3 characters long**: ```php id; - } // ... @@ -515,9 +490,11 @@ final class Profile extends AggregateRoot } ``` -Diese Regel, mit der länge des Namens, ist derzeit nur in changeName definiert. -Damit diese Regel auch beim erstellen greift, muss diese entweder auch in `create` implementiert werden -oder besser, man erstellt ein Value Object dafür, um dafür zu sorgen, dass diese Regel eingehalten wird. +We have now ensured that this rule takes effect when a name is changed with the method `changeName`. +But when we create a new profile this rule does not currently apply. + +In order for this to work, we either have to duplicate the rule or outsource it. +Here we show how we can do it all with a value object: ```php id; - } public static function create(string $id, Name $name): self { @@ -599,8 +571,8 @@ final class Profile extends AggregateRoot } ``` -Damit das ganze auch funktioniert, müssen wir unser Event noch anpassen, -damit es als json serialisierbar ist. +In order for the whole thing to work, we still have to adapt our `NameChanged` event, +since we only expected a string before but now passed a `Name` value object. ```php :warning: The payload must be serializable and unserializable as json. + +There are also cases where business rules have to be defined depending on the aggregate state. +Sometimes also from states, which were changed in the same method. +This is not a problem, as the `apply` methods are always executed immediately. + +In the next case we throw an exception if the hotel is already overbooked. +Besides that, we record another event `FullyBooked`, if the hotel is fully booked with the last booking. +With this event we could notify external systems or fill a projection with fully booked hotels. ```php :warning: In this example we are using the non-strict apply method +> and we are not responding to the event in our aggregate. +> There are cases where events are recorded for external listeners, +> but they are not used in the aggregate themselves. From e9a640d1f9a8d9ebe7a904280ca32807e7d994eb Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 20:24:42 +0100 Subject: [PATCH 14/16] finish pipeline docs --- docs/aggregate.md | 10 ++-- docs/pipeline.md | 149 +++++++++++++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 61 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index a8ffa0155..7076be346 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -134,7 +134,7 @@ final class ProfileCreated extends AggregateChanged > :warning: The payload must be serializable and unserializable as json. > In other words, it can only consist of simple data types (no objects). -> :book: We recommend using named constructors and methods with typehints, +> :book: We recommend using **named constructors** and methods with **typehints**, > so that handling becomes easier and less error-prone. After we have defined the event, we have to adapt the creation of the profile: @@ -183,7 +183,7 @@ final class Profile extends AggregateRoot } ``` -In our named constructor `create` we have now created the event and recorded it with the method` record`. +In our named constructor `create` we have now created the event and recorded it with the method `record`. The aggregate remembers all recorded events in order to save them later. At the same time, the `apply` method is executed directly so that we can change our state. @@ -349,7 +349,7 @@ and prefixing it with an `apply`. Let's assume that the `App\Profile\Event\NameChanged` event is recorded. The short class name in this case would be `NameChanged`. -If we prefix the whole thing with `apply`, we get the method` applyNameChanged`. +If we prefix the whole thing with `apply`, we get the method `applyNameChanged`. This method is then called automatically and the event is passed. If the method does not exist then an error is thrown. @@ -444,8 +444,6 @@ As soon as an event was recorded, the described thing happened and cannot be und A further check in the apply method is also not possible because these events have already happened and were then also saved in the database. -> :warning: Disregarding this can break the rebuilding of the state. - In the next example we want to make sure that **the name is at least 3 characters long**: ```php @@ -490,6 +488,8 @@ final class Profile extends AggregateRoot } ``` +> :warning: Disregarding this can break the rebuilding of the state! + We have now ensured that this rule takes effect when a name is changed with the method `changeName`. But when we create a new profile this rule does not currently apply. diff --git a/docs/pipeline.md b/docs/pipeline.md index fc7e3f287..ebc2522e2 100644 --- a/docs/pipeline.md +++ b/docs/pipeline.md @@ -1,13 +1,13 @@ # Pipeline -Ein Store ist immutable, sprich es darf nicht mehr nachträglich verändert werden. -Dazu gehört sowohl das Manipulieren von Events als auch das Löschen. +A store is immutable, i.e. it cannot be changed afterwards. +This includes both manipulating events and deleting them. -Stattdessen kann man den Store duplizieren und dabei die Events manipulieren. -Somit bleibt der alte Store unberührt und man kann vorher den neuen Store durchtesten, -ob die Migration funktioniert hat. +Instead, you can duplicate the store and manipulate the events in the process. +Thus the old store remains untouched and you can test the new store beforehand, +whether the migration worked. -In diesem Beispiel wird das Event `PrivacyAdded` entfernt und das Event `OldVisited` durch `NewVisited` ersetzt. +In this example the event `PrivacyAdded` is removed and the event `OldVisited` is replaced by `NewVisited`: ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; @@ -30,7 +30,10 @@ $pipeline = new Pipeline( ); ``` -Mit der Pipeline kann man auch Projektion erstellen oder neu aufbauen: +> :warning: Under no circumstances may the same store be used that is used for the source. +> Otherwise the store will be broken afterwards! + +The pipeline can also be used to create or rebuild a projection: ```php use Patchlevel\EventSourcing\Pipeline\Pipeline; @@ -43,20 +46,24 @@ $pipeline = new Pipeline( ); ``` -Das Prinzip bleibt dabei gleich. Es gibt eine Source, woher die Daten kommen. -Ein Target wohin die Daten fließen sollen. -Und beliebig viele Middlewares um mit den Daten vorher irgendetwas anzustellen. +The principle remains the same. +There is a source where the data comes from. +A target where the data should flow. +And any number of middlewares to do something with the data beforehand. + +## EventBucket + +The pipeline works with so-called `EventBucket`. +This `EventBucket` wraps the event or `AggregateChanged` and adds further meta information +like the `aggregateClass` and the event `index`. ## Source -Als Erstes braucht man eine Quelle. Derzeit gibt es nur den `StoreSource` und `InMemorySource` als Quelle. -Ihr könnt aber jederzeit eigene Sources hinzufügen, -indem ihr das Interface `Patchlevel\EventSourcing\Pipeline\Source\Source` implementiert. -Hier könnt ihr zB. auch von anderen Event-Sourcing Systeme migrieren. +The first thing you need is a source of where the data should come from. ### Store -Der StoreSource ist die standard Quelle um alle Events aus der Datenbank zu laden. +The `StoreSource` is the standard source to load all events from the database. ```php use Patchlevel\EventSourcing\Pipeline\Source\StoreSource; @@ -66,7 +73,7 @@ $source = new StoreSource($store); ### In Memory -Den InMemorySource kann dazu verwendet werden, um tests zu schreiben. +There is an `InMemorySource` that receives the events in an array. This source can be used to write pipeline tests. ```php use Patchlevel\EventSourcing\Pipeline\EventBucket; @@ -81,30 +88,54 @@ $source = new InMemorySource([ ]); ``` +### Custom Source + +You can also create your own source class. It has to inherit from `Source`. +Here you can, for example, create a migration from another event sourcing system or similar system. + +```php +use Patchlevel\EventSourcing\Pipeline\Source\Source; +use Patchlevel\EventSourcing\Pipeline\EventBucket; + +$source = new class implements Source { + /** + * @return Generator + */ + public function load(): Generator + { + yield new EventBucket(Profile::class, 0, new ProfileCreated('1', ['name' => 'David'])); + } + + public function count(): int + { + reutrn 1; + } +} +``` + ## Target -"Ziele" dienen dazu, um die Daten am Ende des Process abzuarbeiten. -Das kann von einem anderen Store bis hin zu Projektionen alles sein. +After you have a source, you still need the destination of the pipeline. ### Store -Als Target kann man einen neuen Store verwendet werden. -Hierbei ist es egal, ob der vorherige Store ein SingleTable oder MultiTable war. -Sprich, man kann auch nachträglich zwischen den beiden Systemen migrieren. - -Wichtig ist aber, dass nicht derselbe Store verwendet wird! -Ein Store ist immutable und darf nur dupliziert werden! +You can use a store to save the final result. ```php use Patchlevel\EventSourcing\Pipeline\Target\StoreTarget; $target = new StoreTarget($store); ``` +> :warning: Under no circumstances may the same store be used that is used for the source. +> Otherwise the store will be broken afterwards! + +> :book: It does not matter whether the previous store was a SingleTable or a MultiTable. +> You can switch back and forth between both store types using the pipeline. ### Projection -Eine Projection kann man auch als Target verwenden, -um zum beispiel eine neue Projection aufzubauen oder eine Projection neu zu bauen. +A projection can also be used as a target. +For example, to set up a new projection or to build a new projection. ```php use Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget; @@ -114,8 +145,9 @@ $target = new ProjectionTarget($projection); ### Projection Repository -Wenn man gleich alle Projections neu bauen oder erzeugen möchte, -dann kann man auch das ProjectionRepositoryTarget verwenden. +If you want to build or create all projections from scratch, +then you can also use the ProjectionRepositoryTarget. +In this, the individual projections are iterated and the events are then passed on. ```php use Patchlevel\EventSourcing\Pipeline\Target\ProjectionRepositoryTarget; @@ -125,7 +157,8 @@ $target = new ProjectionRepositoryTarget($projectionRepository); ### In Memory -Für test zwecke kann man hier auch den InMemoryTarget verwenden. +There is also an in-memory variant for the target. This target can also be used for tests. +With the `buckets` method you get all `EventBuckets` that have reached the target. ```php use Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget; @@ -139,16 +172,15 @@ $buckets = $target->buckets(); ## Middlewares -Um Events bei dem Prozess zu manipulieren, löschen oder zu erweitern, kann man Middelwares verwenden. -Dabei ist wichtig zu wissen, dass einige Middlewares eine recalculation vom playhead erfordert. -Das ist eine Nummerierung der Events, die aufsteigend sein muss. -Ein dem entsprechenden Hinweis wird bei jedem Middleware mitgeliefert. +Middelwares can be used to manipulate, delete or expand events during the process. -### exclude +> :warning: It is important to know that some middlewares require recalculation from the playhead. +> This is a numbering of the events that must be in ascending order. +> A corresponding note is supplied with every middleware. -Mit dieser Middleware kann man bestimmte Events ausschließen. +### exclude -> :warning: ein recalculation vom Playhead ist notwendig! +With this middleware you can exclude certain events. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; @@ -156,12 +188,12 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware; $middleware = new ExcludeEventMiddleware([EmailChanged::class]); ``` -### include +> :warning: After this middleware, the playhead must be recalculated! +### include -Mit dieser Middleware kann man nur bestimmte Events erlauben. -> :warning: ein recalculation vom Playhead ist notwendig! +With this middleware you can only allow certain events. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; @@ -169,12 +201,13 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware; $middleware = new IncludeEventMiddleware([ProfileCreated::class]); ``` -### filter +> :warning: After this middleware, the playhead must be recalculated! -Wenn die standard Filter Möglichkeiten nicht ausreichen, kann man auch einen eigenen Filter schreiben. -Dieser verlangt ein boolean als Rückgabewert. `true` um Events zu erlauben, `false` um diese nicht zu erlauben. +### filter -> :warning: ein recalculation vom Playhead ist notwendig! +If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient, +you can also write your own filter. +This middleware expects a callback that returns either true to allow events or false to not allow them. ```php use Patchlevel\EventSourcing\Aggregate\AggregateChanged; @@ -189,14 +222,13 @@ $middleware = new FilterEventMiddleware(function (AggregateChanged $event) { }); ``` +> :warning: After this middleware, the playhead must be recalculated! ### replace -Wenn man ein Event ersetzen möchte, kann man den ReplaceEventMiddleware verwenden. -Als ersten Parameter muss man die Klasse definieren, die man ersetzen möchte. -Und als zweiten Parameter ein Callback, -dass den alten Event erwartet und ein neues Event zurückliefert. -Die Middleware übernimmt dabei den playhead und recordedAt informationen. +If you want to replace an event, you can use the `ReplaceEventMiddleware`. +The first parameter you have to define is the event class that you want to replace. +And as a second parameter a callback, that the old event awaits and a new event returns. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; @@ -206,10 +238,13 @@ $middleware = new ReplaceEventMiddleware(OldVisited::class, static function (Old }); ``` +> :book: The middleware takes over the playhead and recordedAt information. + ### class rename -Wenn ein Mapping nicht notwendig ist und man nur die Klasse umbenennen möchte -(zB. wenn Namespaces sich geändert haben), dann kann man den ClassRenameMiddleware verwenden. +When mapping is not necessary and you just want to rename the class +(e.g. if namespaces have changed), then you can use the `ClassRenameMiddleware`. +You have to pass a hash map. The key is the old class name and the value is the new class name. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; @@ -221,8 +256,8 @@ $middleware = new ClassRenameMiddleware([ ### until -Ein usecase könnte auch sein, dass man sich die Projektion aus einem vergangenen Zeitpunkt anschauen möchte. -Dabei kann man den UntilEventMiddleware verwenden um nur Events zu erlauben, die vor diesem Zeitpunkt recorded wurden. +A use case could also be that you want to look at the projection from a previous point in time. +You can use the `UntilEventMiddleware` to only allow events that were `recorded` before this point in time. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware; @@ -232,9 +267,9 @@ $middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:0 ### recalculate playhead -Mit dieser Middleware kann man den Playhead neu berechnen lassen. -Dieser muss zwingend immer aufsteigend sein, damit das System weiter funktioniert. -Man kann diese Middleware als letztes hinzufügen. +This middleware can be used to recalculate the playhead. +The playhead must always be in ascending order so that the data is valid. +Some middleware can break this order and the middleware `RecalculatePlayheadMiddleware` can fix this problem. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; @@ -242,9 +277,11 @@ use Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware; $middleware = new RecalculatePlayheadMiddleware(); ``` +> :book: You only need to add this middleware once at the end of the pipeline. + ### chain -Wenn man seine Middlewares Gruppieren möchte, kann man dazu eine oder mehrere ChainMiddlewares verwenden. +If you want to group your middleware, you can use one or more `ChainMiddleware`. ```php use Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware; From dcc57a4326e6622e232a355908e5f8ef8f27213d Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 20:39:55 +0100 Subject: [PATCH 15/16] finish process docs --- docs/processor.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/processor.md b/docs/processor.md index a90449090..2e7a279a2 100644 --- a/docs/processor.md +++ b/docs/processor.md @@ -1,5 +1,13 @@ # Processor +A `processor` is an event listener who listens to recorded events. + +In this library there is a core module called `EventBus`. +For all events that are persisted (when the `save` method has been executed on the repository), +the event will be dispatched to the EventBus. All listeners are then called for each event. + +A process can be for example used to send an email when a profile has been created: + ```php :warning: If you only want to listen to certain events, then you have to check it in the `__invoke` method. + +## Register Processor + +If you are using the `DefaultEventBus`, you can register the listener as follows. + +```php +$eventStream = new DefaultEventBus(); +$eventStream->addListener(new SendEmailListener($mailer)); +``` \ No newline at end of file From 7d6584526fba50289bffa2a3ad761aecb5259e35 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 11 Dec 2021 20:42:41 +0100 Subject: [PATCH 16/16] fix link --- docs/aggregate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 7076be346..0eb1b8823 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -3,7 +3,7 @@ > Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects > that can be treated as a single unit. [...] > -> :book: [DDD Aggregate - Martin Flower](https://martinflower.com/bliki/DDD_Aggregate.html) +> :book: [DDD Aggregate - Martin Flower](https://martinfowler.com/bliki/DDD_Aggregate.html) An AggregateRoot has to inherit from `AggregateRoot` and implement the methods `aggregateRootId` and `apply`. `aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity.