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.md b/docs/pages/aggregate.md index cc4ae5ff9..76021100b 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). @@ -648,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/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/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 diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md index fe2d2af7b..bd3178430 100644 --- a/docs/pages/event_bus.md +++ b/docs/pages/event_bus.md @@ -69,78 +69,71 @@ $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 + The order in which the listeners are executed is determined by the order in which they are passed to the factory. -You can also use the [symfony message bus](https://symfony.com/doc/current/components/messenger.html) -which is much more powerful. - -To use the optional symfony messenger you first have to `install` the packet. - -```bash -composer require symfony/messenger -``` +## Listener provider -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 ]); + +$eventBus = new DefaultEventBus($listenerProvider); ``` -!!! note +!!! tip - 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). + The `DefaultEventBus::create` method uses the `AttributeListenerProvider` by default. -Or plug it together by hand: +### Custom listener provider -```php -use Patchlevel\EventSourcing\EventBus\SymfonyEventBus; +You can also use your own listener provider. -$symfonyMessenger = //... +```php +use Patchlevel\EventSourcing\EventBus\DefaultEventBus; +use Patchlevel\EventSourcing\EventBus\ListenerProvider; +use Patchlevel\EventSourcing\EventBus\ListenerDescriptor; -$eventBus = new SymfonyEventBus($symfonyMessenger); +$listenerProvider = new class implements ListenerProvider { + public function listenersForEvent(string $eventClass): iterable + { + return [ + new ListenerDescriptor( + (new WelcomeSubscriber())->onProfileCreated(...), + ), + ]; + } +}; ``` -!!! warning - - 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. - -!!! note +!!! tip - 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). + You can use `$listenerDiscriptor->name()` to get the name of the listener. ## Listener You can listen for specific events with the attribute `Subscribe`. This listener is then called for all saved events / messages. - ```php use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\EventBus\Listener; @@ -155,3 +148,50 @@ final class WelcomeSubscriber } } ``` + +!!! tip + + If you use psalm, you can use the [event sourcing plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) for better type support. + +### 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; +use Patchlevel\EventSourcing\EventBus\Listener; +use Patchlevel\EventSourcing\EventBus\Message; + +final class WelcomeSubscriber +{ + #[Subscribe('*')] + public function onProfileCreated(Message $message): void + { + echo 'Welcome!'; + } +} +``` + +## 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) +* [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) 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 diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md index b84a936a1..4ef9dadf8 100644 --- a/docs/pages/getting_started.md +++ b/docs/pages/getting_started.md @@ -398,4 +398,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 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) 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