From 60d14c9d61da6a4821dc45658c4d2761ce329111 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 17:55:58 +0100 Subject: [PATCH 1/7] improve aggregate docs --- docs/pages/aggregate.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md index cc4ae5ff9..adf0aa6f1 100644 --- a/docs/pages/aggregate.md +++ b/docs/pages/aggregate.md @@ -1,8 +1,8 @@ # Aggregate The linchpin of event-sourcing is the aggregate. These aggregates can be imagined like entities in ORM. -One main difference is that we don't save the current state, but only the individual events that led to the state. -This means it is always possible to build the state again from the events. +One main difference is that we don't save the current state, but only the individual events that led to the state. +This means it is always possible to build the current state again from the events. !!! note @@ -115,6 +115,7 @@ final class ProfileRegistered !!! note You can find out more about events [here](./events.md). + And for normalizer [here](./normalizer.md). After we have defined the event, we have to adapt the profile aggregate: @@ -270,6 +271,10 @@ final class ChangeNameHandler } ``` +!!! success + + Our aggregate can now be changed and saved. + !!! note You can read more about Repository [here](./repository.md). From b19329e4d1d1d0c7afa090187b1e3c2e3338c77c Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 18:37:53 +0100 Subject: [PATCH 2/7] add learn more section --- docs/pages/aggregate.md | 10 +++++++++- docs/pages/events.md | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md index adf0aa6f1..76021100b 100644 --- a/docs/pages/aggregate.md +++ b/docs/pages/aggregate.md @@ -653,4 +653,12 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegist use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; $aggregateRegistry = (new AttributeEventRegistryFactory())->create($paths); -``` \ No newline at end of file +``` + +## Learn more + +* [How to create own aggregate id](aggregate_id.md) +* [How to store and load aggregates](repository.md) +* [How to snapshot aggregates](snapshots.md) +* [How to create Projections](projection.md) +* [How to split streams](split_stream.md) \ No newline at end of file diff --git a/docs/pages/events.md b/docs/pages/events.md index 5a3f8690e..da2682cc1 100644 --- a/docs/pages/events.md +++ b/docs/pages/events.md @@ -112,3 +112,12 @@ use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; $eventRegistry = (new AttributeEventRegistryFactory())->create($paths); ``` + +## Learn more + +* [How to normalize events](normalizer.md) +* [How to dispatch events](event_bus.md) +* [How to listen on events](processor.md) +* [How to store events](store.md) +* [How to split streams](split_stream.md) +* [How to upcast events](upcasting.md) \ No newline at end of file From 80f5c1b8f10cb749c1454709e1ff7e25b69bbad3 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 19:59:59 +0100 Subject: [PATCH 3/7] rewrite aggregate id docs --- docs/mkdocs.yml | 4 +- docs/pages/aggregate_id.md | 210 ++++++++++++++++------------ docs/pages/{tests.md => testing.md} | 0 3 files changed, 125 insertions(+), 89 deletions(-) rename docs/pages/{tests.md => testing.md} (100%) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6086180d6..0b14915ef 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -86,6 +86,7 @@ nav: - Processor: processor.md - Projection: projection.md - Advanced: + - Aggregate ID: aggregate_id.md - Normalizer: normalizer.md - Snapshots: snapshots.md - Upcasting: upcasting.md @@ -94,9 +95,8 @@ nav: - Message Decorator: message_decorator.md - Split Stream: split_stream.md - Time / Clock: clock.md + - Testing: testing.md - Other / Tools: - - UUID: uuid.md - CLI: cli.md - Schema Migration: migration.md - Watch Server: watch_server.md - - Tests: tests.md diff --git a/docs/pages/aggregate_id.md b/docs/pages/aggregate_id.md index e44000a8a..dc0219074 100644 --- a/docs/pages/aggregate_id.md +++ b/docs/pages/aggregate_id.md @@ -1,135 +1,171 @@ -# UUID +# Aggregate ID -A UUID can be generated for the `aggregateId`. There are two popular libraries that can be used: +The `aggregate id` is a unique identifier for an aggregate. +It is used to identify the aggregate in the event store. +The `aggregate` does not care how the id is generated, +since only an aggregate-wide unique string is expected in the store. -* [ramsey/uuid](https://github.com/ramsey/uuid) -* [symfony/uid](https://symfony.com/doc/current/components/uid.html) +This library provides you with a few options for generating the id. -The `aggregate` does not care how the id is generated, since only an aggregate-wide unique string is expected here. +## Uuid + +The easiest way is to use an `uuid` as an aggregate ID. +For this, we have the `Uuid` class, which is a simple wrapper for the [ramsey/uuid](https://github.com/ramsey/uuid) library. + +You can use it like this: ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; +use Patchlevel\EventSourcing\Aggregate\Uuid; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; -use Ramsey\Uuid\Uuid; -use Ramsey\Uuid\UuidInterface; +use Patchlevel\EventSourcing\Attribute\Id; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { - private UuidInterface $id; - private string $name; + #[Id] + #[IdNormalizer(Uuid::class)] + private Uuid $id; +} +``` - public function aggregateRootId(): string - { - return $this->id->toString(); - } - - public function id(): UuidInterface - { - return $this->id; - } - - public function name(): string - { - return $this->name; - } +!!! note - public static function create(string $name): self - { - $id = Uuid::uuid4(); - - $self = new self(); - $self->recordThat(new ProfileCreated($id, $name)); + If you want to use snapshots, then you have to make sure that the aggregate id are normalized. + You can find how to do this [here](normalizer.md). - return $self; - } - - #[Apply] - protected function applyProfileCreated(ProfileCreated $event): void - { - $this->id = $event->profileId(); - $this->name = $event->name(); - } +You have multiple options for generating an uuid: + +```php +use Patchlevel\EventSourcing\Aggregate\Uuid; + +$uuid = Uuid::v6(); +$uuid = Uuid::v7(); +$uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e'); +``` + +!!! Note + + We offer you the uuid versions 6 and 7, because they are the most suitable for event sourcing. + More information about uuid versions can be found [here](https://uuid.ramsey.dev/en/stable/rfc4122.html). + +## Custom ID + +If you don't want to use an uuid, you can also use the custom ID implementation. +This is a value object that holds any string. + +```php +use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; +use Patchlevel\EventSourcing\Aggregate\CustomId; +use Patchlevel\EventSourcing\Attribute\Aggregate; +use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; + +#[Aggregate('profile')] +final class Profile extends BasicAggregateRoot +{ + #[Id] + #[IdNormalizer(CustomId::class)] + private CustomId $id; } ``` -Or even better, you create your own aggregate-specific id class. +!!! note + + If you want to use snapshots, then you have to make sure that the aggregate id are normalized. + You can find how to do this [here](normalizer.md). + +So you can use any string as an id: + +```php +use Patchlevel\EventSourcing\Aggregate\CustomId; + +$id = CustomId::fromString('my-id'); +``` + +## Implement own ID + +Or even better, you create your own aggregate-specific ID class. This allows you to ensure that the correct id is always used. The whole thing looks like this: ```php -use Ramsey\Uuid\Uuid; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; -class ProfileId +class ProfileId implements AggregateRootId { - private string $id; - - public function __construct(string $id) - { - $this->id = $id; - } - - public static function generate(): self - { - return new self(Uuid::uuid4()->toString()); + private function __construct( + private readonly string $id + ){ } - + public function toString(): string { return $this->id; } + + public static function fromString(string $id): self + { + return new self($id); + } } ``` +So you can use it like this: + ```php use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; - -use Ramsey\Uuid\UuidInterface; +use Patchlevel\EventSourcing\Attribute\Id; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { + #[Id] + #[IdNormalizer(ProfileId::class)] private ProfileId $id; - private string $name; +} +``` - public function aggregateRootId(): string - { - return $this->id->toString(); - } - - public function id(): ProfileId - { - return $this->id; - } - - public function name(): string - { - return $this->name; - } +!!! note - public static function create(string $name): self - { - $id = ProfileId::generate(); - - $self = new self(); - $self->recordThat(new ProfileCreated($id, $name)); + If you want to use snapshots, then you have to make sure that the aggregate id are normalized. + You can find how to do this [here](normalizer.md). - return $self; - } - - #[Apply] - protected function applyProfileCreated(ProfileCreated $event): void - { - $this->id = $event->profileId(); - $this->name = $event->name(); - } +We also offer you some traits, so that you don't have to implement the `AggregateRootId` interface yourself. +Here for the uuid: + +```php +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; +use Patchlevel\EventSourcing\Aggregate\RamseyUuidBehaviour; +use Patchlevel\EventSourcing\Aggregate\Uuid; + +class ProfileId implements AggregateRootId +{ + use RamseyUuidBehaviour; } ``` -!!! note +Or for the custom id: - If you want to use snapshots, then you have to make sure that the value objects are normalized. - You can find how to do this [here](normalizer.md). +```php +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; +use Patchlevel\EventSourcing\Aggregate\CustomIdBehaviour; + +class ProfileId implements AggregateRootId +{ + use CustomIdBehaviour; +} +``` + +### Learn more + +* [How to create an aggregate](aggregate.md) +* [How to create an event](events.md) +* [How to test an aggregate](testing.md) +* [How to normalize value objects](normalizer.md) \ No newline at end of file diff --git a/docs/pages/tests.md b/docs/pages/testing.md similarity index 100% rename from docs/pages/tests.md rename to docs/pages/testing.md From 617b3ffcce0cd2b1026588ba2284d70f4f553e6e Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 20:07:13 +0100 Subject: [PATCH 4/7] update clock docs --- docs/pages/clock.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/pages/clock.md b/docs/pages/clock.md index f10c1105f..41f884310 100644 --- a/docs/pages/clock.md +++ b/docs/pages/clock.md @@ -1,13 +1,13 @@ # Clock -We are using the clock to get the current datetime. This is needed to create the `recorded_on` datetime for the events. +We are using the clock to get the current datetime. This is needed to create the `recorded_on` datetime for the event stream. We have two implementations of the clock, one for the production and one for the tests. But you can also create your own implementation that is PSR-20 compatible. For more information see [here](https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md). ## SystemClock -This uses the native system clock to return the DateTimeImmutable instance - in this case `new DateTimeImmutable()`. +This uses the native system clock to return the `DateTimeImmutable` instance. ```php use Patchlevel\EventSourcing\Clock\SystemClock; @@ -67,4 +67,11 @@ $clock->sleep(10); // sleep 10 seconds !!! note - The instance of the frozen datetime will be cloned internally, so the it's not the same instance but equals. \ No newline at end of file + The instance of the frozen datetime will be cloned internally, so the it's not the same instance but equals. + +## Learn more + +* [How to test with datetime](testing.md) +* [How to normalize datetime](normalizer.md) +* [How to use messages](event_bus.md) +* [How to decorate messages](message_decorator.md) \ No newline at end of file From 1719374395bbdd6784bc2f26bc3ada1b16703aa9 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 20:09:50 +0100 Subject: [PATCH 5/7] add learn more in getting started --- docs/pages/getting_started.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index 2567d0daa..d875c0a54 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -399,4 +399,13 @@ $hotels = $hotelProjection->getHotels(); We have successfully implemented and used event sourcing. Feel free to browse further in the documentation for more detailed information. - If there are still open questions, create a ticket on Github and we will try to help you. \ No newline at end of file + If there are still open questions, create a ticket on Github and we will try to help you. + +## Learn more + +* [How to create an aggregate](aggregate.md) +* [How to create an event](events.md) +* [How to store aggregates](repository.md) +* [How to process events](processor.md) +* [How to create a projection](projection.md) +* [How to setup the database](store.md) \ No newline at end of file From 17dbead18e8d6e6807ebf11780da53468f55e9e9 Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 21:00:56 +0100 Subject: [PATCH 6/7] update event bus doc --- docs/pages/event_bus.md | 90 ++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index fe2d2af7b..ba03cd3d1 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -69,77 +69,69 @@ $message->customHeaders(); // ['application-id' => 'app'] ## Event Bus -### Default event bus - -The library also delivers a light-weight event bus for which you can register listeners/subscribers and dispatch events. +The event bus is responsible for dispatching the messages to the listeners. +The library also delivers a light-weight event bus for which you can register listeners and dispatch events. ```php use Patchlevel\EventSourcing\EventBus\DefaultEventBus; -$eventBus = new DefaultEventBus(); -$eventBus->addListener($mailListener); -$eventBus->addListener($projectionListener); +$eventBus = DefaultEventBus::create([ + $mailListener, +]); ``` !!! note - You can determine the order in which the listeners are executed. For example, - you can also add listeners after `ProjectionListener` - to access the [projections](./projection.md). - -### Symfony event bus - -You can also use the [symfony message bus](https://symfony.com/doc/current/components/messenger.html) -which is much more powerful. + The order in which the listeners are executed is determined by the order in which they are passed to the factory. -To use the optional symfony messenger you first have to `install` the packet. +## Listener provider -```bash -composer require symfony/messenger -``` - -You can either let us build it with the `create` factory: +The listener provider is responsible for finding all listeners for a specific event. +The default listener provider uses attributes to find the listeners. ```php -use Patchlevel\EventSourcing\EventBus\SymfonyEventBus; +use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\EventBus\AttributeListenerProvider; -$eventBus = SymfonyEventBus::create([ +$listenerProvider = new AttributeListenerProvider([ $mailListener, - $projectionListener ]); -``` -!!! note - - You can determine the order in which the listeners are executed. For example, - you can also add listeners after `ProjectionListener` - to access the [projections](./projection.md). +$eventBus = new DefaultEventBus($listenerProvider); +``` -Or plug it together by hand: +!!! tip -```php -use Patchlevel\EventSourcing\EventBus\SymfonyEventBus; + The `DefaultEventBus::create` method uses the `AttributeListenerProvider` by default. -$symfonyMessenger = //... -$eventBus = new SymfonyEventBus($symfonyMessenger); -``` +## Listener -!!! warning +You can listen for specific events with the attribute `Subscribe`. +This listener is then called for all saved events / messages. - You can't mix it with a command bus. - You should create a [new bus](https://symfony.com/doc/current/messenger/multiple_buses.html) for it. +```php +use Patchlevel\EventSourcing\Attribute\Subscribe; +use Patchlevel\EventSourcing\EventBus\Listener; +use Patchlevel\EventSourcing\EventBus\Message; -!!! note +final class WelcomeSubscriber +{ + #[Subscribe(ProfileCreated::class)] + public function onProfileCreated(Message $message): void + { + echo 'Welcome!'; + } +} +``` - An event bus can have zero or more listeners on an event. - You should allow no handler in the [HandleMessageMiddleware](https://symfony.com/doc/current/components/messenger.html). +!!! tip -## Listener + If you use psalm, you can use the [event sourcing plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) for better type support. -You can listen for specific events with the attribute `Subscribe`. -This listener is then called for all saved events / messages. +### Listen on all events +If you want to listen on all events, you can pass `*` or `Subscribe::ALL` instead of the event class. ```php use Patchlevel\EventSourcing\Attribute\Subscribe; @@ -148,10 +140,18 @@ use Patchlevel\EventSourcing\EventBus\Message; final class WelcomeSubscriber { - #[Subscribe(ProfileCreated::class)] + #[Subscribe('*')] public function onProfileCreated(Message $message): void { echo 'Welcome!'; } } ``` + +## Learn more + +* [How to decorate messages](message_decorator.md) +* [How to use outbox pattern](outbox.md) +* [How to use processor](processor.md) +* [How to use projections](projection.md) +* [How to debug messages with the watch server](watch_server.md) From c4abcc21bb0185a1768a287bf5a9c43cbaec5d2a Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 1 Feb 2024 21:26:41 +0100 Subject: [PATCH 7/7] update message decorator --- docs/pages/event_bus.md | 40 +++++++++++++++++++++++++++++++++ docs/pages/message_decorator.md | 15 ++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index ba03cd3d1..bd3178430 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -104,6 +104,30 @@ $eventBus = new DefaultEventBus($listenerProvider); The `DefaultEventBus::create` method uses the `AttributeListenerProvider` by default. +### Custom listener provider + +You can also use your own listener provider. + +```php +use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\EventBus\ListenerProvider; +use Patchlevel\EventSourcing\EventBus\ListenerDescriptor; + +$listenerProvider = new class implements ListenerProvider { + public function listenersForEvent(string $eventClass): iterable + { + return [ + new ListenerDescriptor( + (new WelcomeSubscriber())->onProfileCreated(...), + ), + ]; + } +}; +``` + +!!! tip + + You can use `$listenerDiscriptor->name()` to get the name of the listener. ## Listener @@ -148,6 +172,22 @@ final class WelcomeSubscriber } ``` +## Psr-14 Event Bus + +You can also use a [psr-14](https://www.php-fig.org/psr/psr-14/) compatible event bus. +In this case, you can't use the `Subscribe` attribute. +You need to use the system of the psr-14 event bus. + +```php +use Patchlevel\EventSourcing\EventBus\Psr14EventBus; + +$eventBus = new Psr14EventBus($psr14EventDispatcher); +``` + +!!! warning + + You can't use the `Subscribe` attribute with the psr-14 event bus. + ## Learn more * [How to decorate messages](message_decorator.md) diff --git a/docs/pages/message_decorator.md b/docs/pages/message_decorator.md index cdaa5c78a..c8b799043 100644 --- a/docs/pages/message_decorator.md +++ b/docs/pages/message_decorator.md @@ -1,6 +1,6 @@ # Message Decorator -There are usecases where you want to add some extra context to your events like metadata which is not directly relevant +There are use-cases where you want to add some extra context to your events like metadata which is not directly relevant for your domain. With `MessageDecorator` we are providing a solution to add this metadata to your events. The metadata will also be persisted in the database and can be retrieved later on. @@ -22,7 +22,7 @@ $decorator = new SplitStreamDecorator($eventMetadataFactory); ### ChainMessageDecorator -To use multiple decorators at the same time, one can use the `ChainMessageDecorator`. +To use multiple decorators at the same time, you can use the `ChainMessageDecorator`. ```php use Patchlevel\EventSourcing\EventBus\Decorator\ChainMessageDecorator; @@ -35,10 +35,12 @@ $decorator = new ChainMessageDecorator([ ## Use decorator -To use the message decorator, you have to pass it to the `DefaultRepositoryManager`. +To use the message decorator, you have to pass it to the `DefaultRepositoryManager`, +which will then pass it to all Repositories. ```php use Patchlevel\EventSourcing\EventBus\Decorator\ChainMessageDecorator; +use Patchlevel\EventSourcing\EventBus\Decorator\SplitStreamDecorator; use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; $decorator = new ChainMessageDecorator([ @@ -84,3 +86,10 @@ final class OnSystemRecordedDecorator implements MessageDecorator !!! tip You can also set multiple headers with `withCustomHeaders` which expects an hashmap. + +## Learn more + +* [How to define events](events.md) +* [How to use the event bus](event_bus.md) +* [How to configure repositories](repository.md) +* [How to upcast events](upcasting.md)