diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index ace6c905..d0ce2db0 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -78,26 +78,26 @@ nav:
- Introduction: index.md
- Getting Started: getting_started.md
- Basics:
- - Aggregate: aggregate.md
- - Events: events.md
- - Repository: repository.md
- - Store: store.md
- - Event Bus: event_bus.md
- - Processor: processor.md
- - Projection: projection.md
+ - Aggregate: aggregate.md
+ - Events: events.md
+ - Repository: repository.md
+ - Store: store.md
+ - Event Bus: event_bus.md
+ - Processor: processor.md
+ - Projection: projection.md
- Advanced:
- - Normalizer: normalizer.md
- - Snapshots: snapshots.md
- - Upcasting: upcasting.md
- - Projectionist: projectionist.md
- - Outbox: outbox.md
- - Pipeline: pipeline.md
- - Message Decorator: message_decorator.md
- - Split Stream: split_stream.md
- - Time / Clock: clock.md
+ - Normalizer: normalizer.md
+ - Snapshots: snapshots.md
+ - Upcasting: upcasting.md
+ - Projectionist: projectionist.md
+ - Outbox: outbox.md
+ - Pipeline: pipeline.md
+ - Message Decorator: message_decorator.md
+ - Split Stream: split_stream.md
+ - Time / Clock: clock.md
- Other / Tools:
- - UUID: uuid.md
- - CLI: cli.md
- - Schema Migration: migration.md
- - Watch Server: watch_server.md
- - Tests: tests.md
+ - UUID: uuid.md
+ - CLI: cli.md
+ - Schema Migration: migration.md
+ - Watch Server: watch_server.md
+ - Tests: tests.md
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index a26c5415..125c71f4 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,10 +1,11 @@
-{% extends "base.html" %}
-
-{% block extrahead %}
-
-
-
-
-
-
-{% endblock %}
\ No newline at end of file
+{% extends "base.html" %} {% block extrahead %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md
index 312be90c..0b423e40 100644
--- a/docs/pages/aggregate.md
+++ b/docs/pages/aggregate.md
@@ -2,10 +2,10 @@
!!! abstract
- Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects
+ 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://martinfowler.com/bliki/DDD_Aggregate.html)
+ [DDD Aggregate - Martin Flower](https://martinfowler.com/bliki/DDD_Aggregate.html)
An Aggregate has to inherit from `AggregateRoot` and need to implement the method `aggregateRootId`.
`aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity.
@@ -28,12 +28,12 @@ final class Profile extends AggregateRoot
{
return $this->id;
}
-
- public static function register(string $id): self
+
+ public static function register(string $id): self
{
$self = new self();
// todo: record create event
-
+
return $self;
}
}
@@ -61,11 +61,11 @@ final class CreateProfileHandler
public function __construct(
private readonly Repository $profileRepository
) {}
-
+
public function __invoke(CreateProfile $command): void
{
$profile = Profile::register($command->id());
-
+
$this->profileRepository->save($profile);
}
}
@@ -121,8 +121,8 @@ final class Profile extends AggregateRoot
{
return $this->id;
}
-
- public function name(): string
+
+ public function name(): string
{
return $this->name;
}
@@ -134,9 +134,9 @@ final class Profile extends AggregateRoot
return $self;
}
-
+
#[Apply]
- protected function applyProfileRegistered(ProfileRegistered $event): void
+ protected function applyProfileRegistered(ProfileRegistered $event): void
{
$this->id = $event->profileId;
$this->name = $event->name;
@@ -152,7 +152,7 @@ In our named constructor `register` we have now created the event and recorded i
The aggregate remembers all new recorded events in order to save them later.
At the same time, a defined apply method is executed directly so that we can change our state.
-So that the AggregateRoot also knows which method it should call,
+So that the AggregateRoot also knows which method it should call,
we have to mark it with the `Apply` [attributes](https://www.php.net/manual/en/language.attributes.overview.php).
We did that in the `applyProfileRegistered` method.
In this method we change the `Profile` properties `id` and `name` with the transferred values.
@@ -179,7 +179,7 @@ final class NameChanged
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.
+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
@@ -197,8 +197,8 @@ final class Profile extends AggregateRoot
{
return $this->id;
}
-
- public function name(): string
+
+ public function name(): string
{
return $this->name;
}
@@ -210,19 +210,19 @@ final class Profile extends AggregateRoot
return $self;
}
-
- public function changeName(string $name): void
+
+ public function changeName(string $name): void
{
$this->recordThat(new NameChanged($name));
}
-
+
#[Apply]
protected function applyProfileRegistered(ProfileRegistered $event): void
{
$this->id = $event->profileId;
$this->name = $event->name;
}
-
+
#[Apply]
protected function applyNameChanged(NameChanged $event): void
{
@@ -231,7 +231,7 @@ final class Profile extends AggregateRoot
}
```
-We have also defined a new `apply` method named `applyNameChanged`
+We have also defined a new `apply` method named `applyNameChanged`
where we change the name depending on the value in the event.
When using it, it can look like this:
@@ -244,16 +244,16 @@ final class ChangeNameHandler
{
private Repository $profileRepository;
- public function __construct(Repository $profileRepository)
+ public function __construct(Repository $profileRepository)
{
$this->profileRepository = $profileRepository;
}
-
+
public function __invoke(ChangeName $command): void
{
$profile = $this->profileRepository->load($command->id());
$profile->changeName($command->name());
-
+
$this->profileRepository->save($profile);
}
}
@@ -271,7 +271,7 @@ The method `changeName` is then executed on the aggregate to change the name.
In this method the event `NameChanged` is generated and recorded.
The `applyNameChanged` method was also called again internally to adjust the state.
-When the `save` method is called on the repository,
+When the `save` method is called on the repository,
all newly recorded events are then fetched and written to the database.
In this specific case only the `NameChanged` changed event.
@@ -291,7 +291,7 @@ final class Profile extends AggregateRoot
private string $name;
// ...
-
+
#[Apply(ProfileCreated::class)]
#[Apply(NameChanged::class)]
protected function applyProfileCreated(ProfileCreated|NameChanged $event): void
@@ -299,7 +299,7 @@ final class Profile extends AggregateRoot
if ($event instanceof ProfileCreated) {
$this->id = $event->profileId;
}
-
+
$this->name = $event->name;
}
}
@@ -307,9 +307,9 @@ final class Profile extends AggregateRoot
## Suppress missing apply methods
-Sometimes you have events that do not change the state of the aggregate itself,
-but are still recorded for the future, to listen on it or to create a projection.
-So that you are not forced to write an apply method for it,
+Sometimes you have events that do not change the state of the aggregate itself,
+but are still recorded for the future, to listen on it or to create a projection.
+So that you are not forced to write an apply method for it,
you can suppress the missing apply exceptions these events with the `SuppressMissingApply` attribute.
```php
@@ -326,7 +326,7 @@ final class Profile extends AggregateRoot
private string $name;
// ...
-
+
#[Apply]
protected function applyProfileCreated(ProfileCreated $event): void
{
@@ -354,7 +354,7 @@ final class Profile extends AggregateRoot
private string $name;
// ...
-
+
#[Apply]
protected function applyProfileCreated(ProfileCreated $event): void
{
@@ -372,11 +372,11 @@ final class Profile extends AggregateRoot
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.
+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.
-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.
+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 the next example we want to make sure that **the name is at least 3 characters long**:
@@ -390,25 +390,25 @@ final class Profile extends AggregateRoot
{
private string $id;
private string $name;
-
+
// ...
-
- public function name(): string
+
+ public function name(): string
{
return $this->name;
}
-
- public function changeName(string $name): void
+
+ public function changeName(string $name): void
{
if (strlen($name) < 3) {
throw new NameIsToShortException($name);
}
-
+
$this->recordThat(new NameChanged($name));
}
-
+
#[Apply]
- protected function applyNameChanged(NameChanged $event): void
+ protected function applyNameChanged(NameChanged $event): void
{
$this->name = $event->name();
}
@@ -419,27 +419,27 @@ final class Profile extends AggregateRoot
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`.
+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.
+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
final class Name
{
private string $value;
-
- public function __construct(string $value)
+
+ public function __construct(string $value)
{
if (strlen($value) < 3) {
throw new NameIsToShortException($value);
}
-
+
$this->value = $value;
}
-
- public function toString(): string
+
+ public function toString(): string
{
return $this->value;
}
@@ -458,7 +458,7 @@ final class Profile extends AggregateRoot
{
private string $id;
private Name $name;
-
+
public static function register(string $id, Name $name): static
{
$self = new static();
@@ -466,28 +466,28 @@ final class Profile extends AggregateRoot
return $self;
}
-
+
// ...
-
- public function name(): Name
+
+ public function name(): Name
{
return $this->name;
}
-
- public function changeName(Name $name): void
+
+ public function changeName(Name $name): void
{
$this->recordThat(new NameChanged($name));
}
-
+
#[Apply]
- protected function applyNameChanged(NameChanged $event): void
+ protected function applyNameChanged(NameChanged $event): void
{
$this->name = $event->name;
}
}
```
-In order for the whole thing to work, we still have to adapt our `NameChanged` event,
+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
@@ -502,6 +502,7 @@ final class NameChanged
) {}
}
```
+
!!! warning
The payload must be serializable and unserializable as json.
@@ -515,8 +516,8 @@ 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](./processor.md) external systems
+Besides that, we record another event `FullyBooked`, if the hotel is fully booked with the last booking.
+With this event we could [notify](./processor.md) external systems
or fill a [projection](./projection.md) with fully booked hotels.
```php
@@ -532,24 +533,24 @@ final class Hotel extends AggregateRoot
private const SIZE = 5;
private int $people;
-
+
// ...
-
- public function book(string $name): void
+
+ public function book(string $name): void
{
if ($this->people === self::SIZE) {
throw new NoPlaceException($name);
}
-
+
$this->recordThat(new RoomBocked($name));
-
+
if ($this->people === self::SIZE) {
$this->recordThat(new FullyBooked());
}
}
-
+
#[Apply]
- protected function applyRoomBocked(RoomBocked $event): void
+ protected function applyRoomBocked(RoomBocked $event): void
{
$this->people++;
}
@@ -558,10 +559,10 @@ final class Hotel extends AggregateRoot
## Working with dates
-An aggregate should always be deterministic. In other words, whenever I execute methods on the aggregate,
+An aggregate should always be deterministic. In other words, whenever I execute methods on the aggregate,
I always get the same result. This also makes testing much easier.
-But that often doesn't seem to be possible, e.g. if you want to save a createAt date.
+But that often doesn't seem to be possible, e.g. if you want to save a createAt date.
But you can pass this information by yourself.
```php
@@ -575,7 +576,7 @@ final class Profile extends AggregateRoot
private string $id;
private Name $name;
private DateTimeImmutable $registeredAt;
-
+
public static function register(string $id, string $name, DateTimeImmutable $registeredAt): static
{
$self = new static();
@@ -583,7 +584,7 @@ final class Profile extends AggregateRoot
return $self;
}
-
+
// ...
}
```
@@ -602,7 +603,7 @@ final class Profile extends AggregateRoot
private string $id;
private Name $name;
private DateTimeImmutable $registeredAt;
-
+
public static function register(string $id, string $name, Clock $clock): static
{
$self = new static();
@@ -610,12 +611,12 @@ final class Profile extends AggregateRoot
return $self;
}
-
+
// ...
}
```
-Now you can pass the `SystemClock` to determine the current time.
+Now you can pass the `SystemClock` to determine the current time.
Or for test purposes the `FrozenClock`, which always returns the same time.
!!! note
@@ -645,4 +646,4 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegist
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
$aggregateRegistry = (new AttributeEventRegistryFactory())->create($paths);
-```
\ No newline at end of file
+```
diff --git a/docs/pages/cli.md b/docs/pages/cli.md
index cdfc36bb..38c7ab0d 100644
--- a/docs/pages/cli.md
+++ b/docs/pages/cli.md
@@ -1,24 +1,24 @@
# CLI
-The library also offers `symfony` cli commands to create or delete `databases`.
-It is also possible to manage the `schema` and `projections`.
-These commands are `optional` and only wrap existing functionalities
+The library also offers `symfony` cli commands to create or delete `databases`.
+It is also possible to manage the `schema` and `projections`.
+These commands are `optional` and only wrap existing functionalities
that are also available in this way.
## Database commands
There are two commands for creating and deleting a database.
-* DatabaseCreateCommand: `event-sourcing:database:create`
-* DatabaseDropCommand: `event-sourcing:database:drop`
+- DatabaseCreateCommand: `event-sourcing:database:create`
+- DatabaseDropCommand: `event-sourcing:database:drop`
## Schema commands
The database schema can also be created, updated and dropped.
-* SchemaCreateCommand: `event-sourcing:schema:create`
-* SchemaUpdateCommand: `event-sourcing:schema:update`
-* SchemaDropCommand: `event-sourcing:schema:drop`
+- SchemaCreateCommand: `event-sourcing:schema:create`
+- SchemaUpdateCommand: `event-sourcing:schema:update`
+- SchemaDropCommand: `event-sourcing:schema:drop`
!!! note
@@ -29,9 +29,9 @@ The database schema can also be created, updated and dropped.
The creation, deletion and rebuilding of the projections is also possible via the cli.
-* ProjectionCreateCommand: `event-sourcing:projection:create`
-* ProjectionDropCommand: `event-sourcing:projection:drop`
-* ProjectionRebuildCommand: `event-sourcing:projection:rebuild`
+- ProjectionCreateCommand: `event-sourcing:projection:create`
+- ProjectionDropCommand: `event-sourcing:projection:drop`
+- ProjectionRebuildCommand: `event-sourcing:projection:rebuild`
!!! note
@@ -41,12 +41,12 @@ The creation, deletion and rebuilding of the projections is also possible via th
To manage your projectors there are the following cli commands.
-* ProjectionistBootCommand: `event-sourcing:projectionist:boot`
-* ProjectionistReactiveCommand: `event-sourcing:projectionist:reactive`
-* ProjectionistRemoveCommand: `event-sourcing:projectionist:remove`
-* ProjectionistRunCommand: `event-sourcing:projectionist:run`
-* ProjectionistStatusCommand: `event-sourcing:projectionist:status`
-* ProjectionistTeardownCommand: `event-sourcing:projectionist:teardown`
+- ProjectionistBootCommand: `event-sourcing:projectionist:boot`
+- ProjectionistReactiveCommand: `event-sourcing:projectionist:reactive`
+- ProjectionistRemoveCommand: `event-sourcing:projectionist:remove`
+- ProjectionistRunCommand: `event-sourcing:projectionist:run`
+- ProjectionistStatusCommand: `event-sourcing:projectionist:status`
+- ProjectionistTeardownCommand: `event-sourcing:projectionist:teardown`
!!! note
@@ -56,8 +56,8 @@ To manage your projectors there are the following cli commands.
Interacting with the outbox store is also possible via the cli.
-* OutboxInfoCommand: `event-sourcing:outbox:info`
-* OutboxConsumeCommand: `event-sourcing:outbox:consume`
+- OutboxInfoCommand: `event-sourcing:outbox:info`
+- OutboxConsumeCommand: `event-sourcing:outbox:consume`
!!! note
@@ -98,5 +98,5 @@ $cli->run();
!!! note
- You can also register doctrine migration commands,
+ You can also register doctrine migration commands,
see the [store](./store.md) documentation for this.
diff --git a/docs/pages/clock.md b/docs/pages/clock.md
index 09a3dbc9..58598804 100644
--- a/docs/pages/clock.md
+++ b/docs/pages/clock.md
@@ -15,7 +15,7 @@ This uses the native system clock to return the DateTimeImmutable instance - in
use Patchlevel\EventSourcing\Clock\SystemClock;
$clock = new SystemClock();
-$date = $clock->now(); // get the actual datetime
+$date = $clock->now(); // get the actual datetime
$date2 = $clock->now();
$date == $date2 // false
@@ -33,7 +33,7 @@ use Patchlevel\EventSourcing\Clock\FrozenClock;
$date = new DateTimeImmutable();
$clock = new FrozenClock($date);
-$frozenDate = $clock->now(); // gets the date provided before
+$frozenDate = $clock->now(); // gets the date provided before
$date == $frozenDate // true
$date === $frozenDate // false
@@ -58,4 +58,4 @@ $secondDate == $frozenDate // true
!!! 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.
diff --git a/docs/pages/event_bus.md b/docs/pages/event_bus.md
index 2b3ece9f..d9d616fe 100644
--- a/docs/pages/event_bus.md
+++ b/docs/pages/event_bus.md
@@ -11,12 +11,12 @@ event/message.
A `Message` contains the event and related meta information such as the aggregate class and id.
A message contains the following information:
-* aggregate class
-* aggregate id
-* playhead
-* event
-* recorded on
-* custom headers
+- aggregate class
+- aggregate id
+- playhead
+- event
+- recorded on
+- custom headers
Each event is packed into a message and dispatched using the event bus.
@@ -83,7 +83,7 @@ $eventBus->addListener($projectionListener);
!!! note
- You can determine the order in which the listeners are executed. For example,
+ 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).
@@ -132,7 +132,7 @@ $eventBus = new SymfonyEventBus($symfonyMessenger);
!!! note
- An event bus can have zero or more listeners on an event.
+ 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).
## Listener
@@ -144,7 +144,7 @@ This listener is then called for all saved events / messages.
use Patchlevel\EventSourcing\EventBus\Listener;
use Patchlevel\EventSourcing\EventBus\Message;
-final class WelcomeListener implements Listener
+final class WelcomeListener implements Listener
{
public function __invoke(Message $message): void
{
@@ -157,12 +157,12 @@ final class WelcomeListener implements Listener
!!! warning
- If you only want to listen to certain messages,
+ If you only want to listen to certain messages,
then you have to check it in the `__invoke` method or use the subscriber.
## Subscriber
-A `Subscriber` is a listener, except that it has implemented the invoke method itself.
+A `Subscriber` is a listener, except that it has implemented the invoke method itself.
Instead, you can define your own and multiple methods and listen for specific events with the attribute `Handle`.
```php
@@ -170,7 +170,7 @@ use Patchlevel\EventSourcing\Attribute\Handle;
use Patchlevel\EventSourcing\EventBus\Listener;
use Patchlevel\EventSourcing\EventBus\Message;
-final class WelcomeSubscriber extends Subscriber
+final class WelcomeSubscriber extends Subscriber
{
#[Handle(ProfileCreated::class)]
public function onProfileCreated(Message $message): void
diff --git a/docs/pages/events.md b/docs/pages/events.md
index 610a6816..db6780f3 100644
--- a/docs/pages/events.md
+++ b/docs/pages/events.md
@@ -1,17 +1,17 @@
# Events
-Events are used to describe things that happened in the application.
-Since the events already happened, they are also immnutable.
-In event sourcing, these are used to save and rebuild the current state.
+Events are used to describe things that happened in the application.
+Since the events already happened, they are also immnutable.
+In event sourcing, these are used to save and rebuild the current state.
You can also listen on events to react and perform different actions.
An event has a name and additional information called payload.
Such an event can be represented as any class.
-It is important that the payload can be serialized as JSON at the end.
+It is important that the payload can be serialized as JSON at the end.
Later it will be explained how to ensure it for all values.
-To register an event you have to set the `Event` attribute over the class,
-otherwise it will not be recognized as an event.
+To register an event you have to set the `Event` attribute over the class,
+otherwise it will not be recognized as an event.
There you also have to give the event a name.
```php
@@ -45,7 +45,7 @@ final class ProfileCreated
## Serializer
So that the events can be saved in the database, they must be serialized and deserialized.
-That's what the serializer is for.
+That's what the serializer is for.
The library comes with a `DefaultEventSerializer` that can be given further instructions using attributes.
```php
@@ -54,14 +54,14 @@ use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
$serializer = DefaultEventSerializer::createFromPaths(['src/Domain']);
```
-The serializer needs the path information where the event classes are located
-so that it can instantiate the correct classes.
+The serializer needs the path information where the event classes are located
+so that it can instantiate the correct classes.
Internally, an EventRegistry is used, which will be described later.
## Normalizer
Sometimes you also want to add more complex data as a payload. For example DateTime or value objects.
-You can do that too. However, you must define a normalizer for this
+You can do that too. However, you must define a normalizer for this
so that the library knows how to write this data to the database and load it again.
```php
@@ -81,11 +81,11 @@ final class NameChanged
!!! note
- You can find out more about normalizer [here](normalizer.md).
+ You can find out more about normalizer [here](normalizer.md).
## Event Registry
-The library needs to know about all events
+The library needs to know about all events
so that the correct event class is used for the serialization and deserialization of an event.
There is an EventRegistry for this purpose. The registry is a simple hashmap between event name and event class.
@@ -98,8 +98,8 @@ $eventRegistry = new EventRegistry([
```
So that you don't have to create it by hand, you can use a factory.
-By default, the `AttributeEventRegistryFactory` is used.
-There, with the help of paths, all classes with the attribute `Event` are searched for
+By default, the `AttributeEventRegistryFactory` is used.
+There, with the help of paths, all classes with the attribute `Event` are searched for
and the `EventRegistry` is built up.
```php
diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md
index 96490b76..334ede60 100644
--- a/docs/pages/getting_started.md
+++ b/docs/pages/getting_started.md
@@ -49,7 +49,7 @@ final class GuestIsCheckedOut
!!! note
- You can find out more about events [here](events.md).
+ You can find out more about events [here](events.md).
## Define aggregates
@@ -68,7 +68,7 @@ final class Hotel extends AggregateRoot
{
private string $id;
private string $name;
-
+
/**
* @var list
*/
@@ -97,35 +97,35 @@ final class Hotel extends AggregateRoot
if (in_array($guestName, $this->guests, true)) {
throw new GuestHasAlreadyCheckedIn($guestName);
}
-
+
$this->recordThat(new GuestIsCheckedIn($guestName));
}
-
+
public function checkOut(string $guestName): void
{
if (!in_array($guestName, $this->guests, true)) {
throw new IsNotAGuest($guestName);
}
-
+
$this->recordThat(new GuestIsCheckedOut($guestName));
}
-
+
#[Apply]
- protected function applyHotelCreated(HotelCreated $event): void
+ protected function applyHotelCreated(HotelCreated $event): void
{
$this->id = $event->hotelId;
$this->name = $event->hotelName;
- $this->guests = [];
+ $this->guests = [];
}
-
+
#[Apply]
- protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void
+ protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void
{
$this->guests[] = $event->guestName;
}
-
+
#[Apply]
- protected function applyGuestIsCheckedOut(GuestIsCheckedOut $event): void
+ protected function applyGuestIsCheckedOut(GuestIsCheckedOut $event): void
{
$this->guests = array_values(
array_filter(
@@ -165,11 +165,11 @@ final class HotelProjection implements Projector
private readonly Connection $db
) {
}
-
+
/**
* @return list
*/
- public function getHotels(): array
+ public function getHotels(): array
{
return $this->db->fetchAllAssociative('SELECT id, name, guests FROM hotel;')
}
@@ -178,17 +178,17 @@ final class HotelProjection implements Projector
public function handleHotelCreated(Message $message): void
{
$event = $message->event();
-
+
$this->db->insert(
- 'hotel',
+ 'hotel',
[
- 'id' => $event->hotelId,
+ 'id' => $event->hotelId,
'name' => $event->hotelName,
'guests' => 0
]
);
}
-
+
#[Handle(GuestIsCheckedIn::class)]
public function handleGuestIsCheckedIn(Message $message): void
{
@@ -197,7 +197,7 @@ final class HotelProjection implements Projector
[$message->aggregateId()]
);
}
-
+
#[Handle(GuestIsCheckedOut::class)]
public function handleGuestIsCheckedOut(Message $message): void
{
@@ -206,7 +206,7 @@ final class HotelProjection implements Projector
[$message->aggregateId()]
);
}
-
+
#[Create]
public function create(): void
{
@@ -358,5 +358,5 @@ $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
+ 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.
diff --git a/docs/pages/index.md b/docs/pages/index.md
index 715d3913..ff3bee3b 100644
--- a/docs/pages/index.md
+++ b/docs/pages/index.md
@@ -4,15 +4,15 @@ A lightweight but also all-inclusive event sourcing library with a focus on deve
## Features
-* Everything is included in the package for event sourcing
-* Based on [doctrine dbal](https://github.com/doctrine/dbal) and their ecosystem
-* Developer experience oriented and fully typed
-* [Snapshots](snapshots.md) and [Split-Stream](split_stream.md) system to quickly rebuild the aggregates
-* [Pipeline](pipeline.md) to build new [projections](projection.md) or to migrate events
-* [Projectionist](projectionist.md) for managed, versioned and asynchronous projections
-* [Scheme management](store.md) and [doctrine migration](migration.md) support
-* Dev [tools](watch_server.md) such as a realtime event watcher
-* Built in [cli commands](cli.md) with [symfony](https://symfony.com/)
+- Everything is included in the package for event sourcing
+- Based on [doctrine dbal](https://github.com/doctrine/dbal) and their ecosystem
+- Developer experience oriented and fully typed
+- [Snapshots](snapshots.md) and [Split-Stream](split_stream.md) system to quickly rebuild the aggregates
+- [Pipeline](pipeline.md) to build new [projections](projection.md) or to migrate events
+- [Projectionist](projectionist.md) for managed, versioned and asynchronous projections
+- [Scheme management](store.md) and [doctrine migration](migration.md) support
+- Dev [tools](watch_server.md) such as a realtime event watcher
+- Built in [cli commands](cli.md) with [symfony](https://symfony.com/)
## Installation
@@ -22,5 +22,5 @@ composer require patchlevel/event-sourcing
## Integration
-* [Symfony](https://github.com/patchlevel/event-sourcing-bundle)
-* [Psalm](https://github.com/patchlevel/event-sourcing-psalm-plugin)
+- [Symfony](https://github.com/patchlevel/event-sourcing-bundle)
+- [Psalm](https://github.com/patchlevel/event-sourcing-psalm-plugin)
diff --git a/docs/pages/message_decorator.md b/docs/pages/message_decorator.md
index 878aff30..69a24c31 100644
--- a/docs/pages/message_decorator.md
+++ b/docs/pages/message_decorator.md
@@ -2,7 +2,7 @@
There are usecases 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.
+will also be persisted in the database and can be retrieved later on.
## Built-in decorator
@@ -10,7 +10,7 @@ We offer a few decorators that you can use.
### RecordedOnDecorator
-Each message needs a `RecordedOn` time. The `RecordedOnDecorator` is needed so that this is added to the message.
+Each message needs a `RecordedOn` time. The `RecordedOnDecorator` is needed so that this is added to the message.
This decorator needs a [clock](clock.md) implementation.
```php
@@ -23,7 +23,7 @@ $decorator = new RecordedOnDecorator($clock);
!!! warning
- A `RecordedOn` time must always be created.
+ A `RecordedOn` time must always be created.
Either this decorator must always be added or an appropriate replacement must be provided.
### SplitStreamDecorator
@@ -62,7 +62,7 @@ use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
$decorator = new ChainMessageDecorator([
new RecordedOnDecorator($clock),
- new SplitStreamDecorator($eventMetadataFactory)
+ new SplitStreamDecorator($eventMetadataFactory)
]);
$repositoryManager = new DefaultRepositoryManager(
@@ -78,8 +78,8 @@ $repository = $repositoryManager->get(Profile::class);
!!! warning
- We also use the decorator to fill in the `RecordedOn` time.
- If you want to add your own decorator or the SplitStreamDecorator,
+ We also use the decorator to fill in the `RecordedOn` time.
+ If you want to add your own decorator or the SplitStreamDecorator,
then you need to make sure to add the `RecordedOnDecorator` as well.
!!! note
@@ -100,7 +100,7 @@ final class OnSystemRecordedDecorator implements MessageDecorator
{
return $message->withCustomHeader('system', 'accounting_system');
}
-}
+}
```
!!! note
diff --git a/docs/pages/migration.md b/docs/pages/migration.md
index 8f67e6e7..c16ef613 100644
--- a/docs/pages/migration.md
+++ b/docs/pages/migration.md
@@ -53,7 +53,7 @@ $connection = DriverManager::getConnection([
$config = new PhpFile('migrations.php');
$dependencyFactory = DependencyFactory::fromConnection(
- $config,
+ $config,
new ExistingConnection($connection)
);
@@ -63,7 +63,7 @@ $schemaDirector = new DoctrineSchemaDirector(
);
$dependencyFactory->setService(
- SchemaProvider::class,
+ SchemaProvider::class,
new DoctrineMigrationSchemaProvider($schemaDirector)
);
@@ -73,7 +73,7 @@ $cli->setCatchExceptions(true);
$cli->addCommands([
// other cli commands
-
+
new Command\ExecuteCommand($dependencyFactory, 'event-sourcing:migrations:execute'),
new Command\GenerateCommand($dependencyFactory, 'event-sourcing:migrations:generate'),
new Command\LatestCommand($dependencyFactory, 'event-sourcing:migrations:latest'),
@@ -89,19 +89,18 @@ $cli->run();
!!! note
- Here you can find more information on how to
+ Here you can find more information on how to
[configure doctrine migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.3/reference/custom-configuration.html).
-
## Migration commands
There are some commands to use the migration feature.
-* ExecuteCommand: `event-sourcing:migrations:execute`
-* GenerateCommand: `event-sourcing:migrations:generate`
-* LatestCommand: `event-sourcing:migrations:latest`
-* ListCommand: `event-sourcing:migrations:list`
-* MigrateCommand: `event-sourcing:migrations:migrate`
-* DiffCommand: `event-sourcing:migrations:diff`
-* StatusCommand: `event-sourcing:migrations:status`
-* VersionCommand: `event-sourcing:migrations:version`
+- ExecuteCommand: `event-sourcing:migrations:execute`
+- GenerateCommand: `event-sourcing:migrations:generate`
+- LatestCommand: `event-sourcing:migrations:latest`
+- ListCommand: `event-sourcing:migrations:list`
+- MigrateCommand: `event-sourcing:migrations:migrate`
+- DiffCommand: `event-sourcing:migrations:diff`
+- StatusCommand: `event-sourcing:migrations:status`
+- VersionCommand: `event-sourcing:migrations:version`
diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md
index 0ae27c00..faba50be 100644
--- a/docs/pages/normalizer.md
+++ b/docs/pages/normalizer.md
@@ -12,7 +12,7 @@ You have to set the normalizer to the properties using the specific normalizer c
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeImmutableNormalizer;
-final class DTO
+final class DTO
{
#[DateTimeImmutableNormalizer]
public DateTimeImmutable $date;
@@ -24,7 +24,7 @@ The whole thing also works with property promotion.
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeImmutableNormalizer;
-final class DTO
+final class DTO
{
public function __construct(
#[DateTimeImmutableNormalizer]
@@ -95,7 +95,7 @@ objects. Internally, it basically does an `array_map` and then runs the specifie
use Patchlevel\EventSourcing\Serializer\Normalizer\ArrayNormalizer;
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeImmutableNormalizer;
-final class DTO
+final class DTO
{
#[ArrayNormalizer(new DateTimeImmutableNormalizer())]
public array $dates;
@@ -114,7 +114,7 @@ you can convert DateTimeImmutable objects to a String and back again.
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeImmutableNormalizer;
-final class DTO
+final class DTO
{
#[DateTimeImmutableNormalizer]
public DateTimeImmutable $date;
@@ -127,7 +127,7 @@ The default is `DateTimeImmutable::ATOM`.
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeImmutableNormalizer;
-final class DTO
+final class DTO
{
#[DateTimeImmutableNormalizer(format: DateTimeImmutable::RFC3339_EXTENDED)]
public DateTimeImmutable $date;
@@ -145,7 +145,7 @@ The `DateTime` Normalizer works exactly like the DateTimeNormalizer. Only for Da
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeNormalizer;
-final class DTO
+final class DTO
{
#[DateTimeNormalizer]
public DateTime $date;
@@ -157,7 +157,7 @@ You can also specify the format here. The default is `DateTime::ATOM`.
```php
use Patchlevel\EventSourcing\Serializer\Normalizer\DateTimeNormalizer;
-final class DTO
+final class DTO
{
#[DateTimeNormalizer(format: DateTime::RFC3339_EXTENDED)]
public DateTime $date;
@@ -166,7 +166,7 @@ final class DTO
!!! warning
- It is highly recommended to only ever use DateTimeImmutable objects and the DateTimeImmutableNormalizer.
+ It is highly recommended to only ever use DateTimeImmutable objects and the DateTimeImmutableNormalizer.
This prevents you from accidentally changing the state of the DateTime and thereby causing bugs.
!!! note
@@ -188,7 +188,7 @@ final class DTO {
### Enum
-Backed enums can also be normalized.
+Backed enums can also be normalized.
For this, the enum FQCN must also be pass so that the `EnumNormalizer` knows which enum it is.
```php
@@ -202,7 +202,7 @@ final class DTO {
## Custom Normalizer
-Since we only offer normalizers for PHP native things,
+Since we only offer normalizers for PHP native things,
you have to write your own normalizers for your own structures, such as value objects.
In our example we have built a value object that should hold a name.
@@ -211,17 +211,17 @@ In our example we have built a value object that should hold a name.
final class Name
{
private string $value;
-
- public function __construct(string $value)
+
+ public function __construct(string $value)
{
if (strlen($value) < 3) {
throw new NameIsToShortException($value);
}
-
+
$this->value = $value;
}
-
- public function toString(): string
+
+ public function toString(): string
{
return $this->value;
}
@@ -311,6 +311,6 @@ The whole thing looks like this
!!! note
- NormalizedName also works for snapshots.
- But since a snapshot is just a cache, you can also just invalidate it,
+ NormalizedName also works for snapshots.
+ But since a snapshot is just a cache, you can also just invalidate it,
if you have backwards compatibility break in the property name
diff --git a/docs/pages/outbox.md b/docs/pages/outbox.md
index 6bd41988..b4675d5b 100644
--- a/docs/pages/outbox.md
+++ b/docs/pages/outbox.md
@@ -1,17 +1,17 @@
# Outbox
-There is the problem that errors can occur when saving an aggregate or in the individual event listeners.
-This means that you either saved an aggregate, but an error occurred in the email listener, so that no email went out.
+There is the problem that errors can occur when saving an aggregate or in the individual event listeners.
+This means that you either saved an aggregate, but an error occurred in the email listener, so that no email went out.
Or that an email was sent but the aggregate could not be saved.
-Both cases are very bad and can only be solved if both the saving of an aggregate
+Both cases are very bad and can only be solved if both the saving of an aggregate
and the dispatching of the events are in a transaction.
-The best way to ensure this is to store the events to be dispatched together
+The best way to ensure this is to store the events to be dispatched together
with the aggregate in a transaction in the same database.
-After the transaction becomes successful, the events can be loaded from the outbox table with a worker
-and then dispatched into the correct event bus. As soon as the events have been dispatched,
+After the transaction becomes successful, the events can be loaded from the outbox table with a worker
+and then dispatched into the correct event bus. As soon as the events have been dispatched,
they are deleted from the outbox table. If an error occurs when dispatching, the whole thing will be retrieved later.
## Configuration
@@ -29,10 +29,10 @@ $repositoryManager = new DefaultRepositoryManager(
$aggregateRootRegistry,
$store,
$outboxEventBus
-);
+);
```
-And then you have to define the consumer. This gets the right event bus.
+And then you have to define the consumer. This gets the right event bus.
It is used to load the events to be dispatched from the database, dispatch the events and then empty the outbox table.
```php
@@ -50,7 +50,7 @@ $store->transactional(function () use ($command, $profileRepository) {
$command->id(),
$command->email()
);
-
+
$profileRepository->save($profile);
});
```
@@ -65,7 +65,7 @@ You can also interact directly with the outbox store.
$store->saveOutboxMessage($message);
$store->markOutboxMessageConsumed($message);
-$store->retrieveOutboxMessages();
+$store->retrieveOutboxMessages();
$store->countOutboxMessages()
```
@@ -75,4 +75,4 @@ $store->countOutboxMessages()
!!! tip
- Interacting with the outbox store is also possible via the [cli](cli.md).
\ No newline at end of file
+ Interacting with the outbox store is also possible via the [cli](cli.md).
diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md
index 8bba2633..fe0aa598 100644
--- a/docs/pages/pipeline.md
+++ b/docs/pages/pipeline.md
@@ -48,7 +48,7 @@ $pipeline = new Pipeline(
);
```
-The principle remains the same.
+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.
@@ -88,7 +88,7 @@ $source = new InMemorySource([
### Custom Source
-You can also create your own source class. It has to inherit from `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
@@ -104,7 +104,7 @@ $source = new class implements Source {
yield new Message(
Profile::class,
'1',
- 0,
+ 0,
new ProfileCreated('1', ['name' => 'David'])
);
}
@@ -132,7 +132,7 @@ $target = new StoreTarget($store);
!!! danger
- Under no circumstances may the same store be used that is used for the source.
+ Under no circumstances may the same store be used that is used for the source.
Otherwise the store will be broken afterwards!
!!! note
@@ -227,7 +227,6 @@ $middleware = new ExcludeEventMiddleware([EmailChanged::class]);
### Include
-
With this middleware you can only allow certain events.
```php
@@ -242,8 +241,8 @@ $middleware = new IncludeEventMiddleware([ProfileCreated::class]);
### Filter
-If the middlewares `ExcludeEventMiddleware` and `IncludeEventMiddleware` are not sufficient,
-you can also write your own filter.
+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
@@ -254,7 +253,7 @@ $middleware = new FilterEventMiddleware(function (AggregateChanged $event) {
if (!$event instanceof ProfileCreated) {
return true;
}
-
+
return $event->allowNewsletter();
});
```
@@ -279,7 +278,6 @@ $middleware = new ExcludeArchivedEventMiddleware();
### Only Archived Events
-
With this middleware you can only allow archived events.
```php
@@ -328,7 +326,7 @@ $middleware = new UntilEventMiddleware(new DateTimeImmutable('2020-01-01 12:00:0
### Recalculate playhead
This middleware can be used to recalculate the playhead.
-The playhead must always be in ascending order so that the data is valid.
+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
@@ -358,17 +356,17 @@ $middleware = new ChainMiddleware([
### Custom middleware
-You can also write a custom middleware. The middleware gets a message and can return `N` messages.
+You can also write a custom middleware. The middleware gets a message and can return `N` messages.
There are the following possibilities:
-* Return only the message to an array to leave it unchanged.
-* Put another message in the array to swap the message.
-* Return an empty array to remove the message.
-* Or return multiple messages to enrich the stream.
+- Return only the message to an array to leave it unchanged.
+- Put another message in the array to swap the message.
+- Return an empty array to remove the message.
+- Or return multiple messages to enrich the stream.
-In our case, the domain has changed a bit.
-In the beginning we had a `ProfileCreated` event that just created a profile.
-Now we have a `ProfileRegistered` and a `ProfileActivated` event,
+In our case, the domain has changed a bit.
+In the beginning we had a `ProfileCreated` event that just created a profile.
+Now we have a `ProfileRegistered` and a `ProfileActivated` event,
which should replace the `ProfileCreated` event.
```php
@@ -380,23 +378,23 @@ final class SplitProfileCreatedMiddleware implements Middleware
public function __invoke(Message $message): array
{
$event = $message->event();
-
+
if (!$event instanceof ProfileCreated) {
return [$message];
}
-
+
$profileRegisteredMessage = Message::createWithHeaders(
- new ProfileRegistered($event->id(), $event->name()),
+ new ProfileRegistered($event->id(), $event->name()),
$message->headers()
);
-
+
$profileActivatedMessage = Message::createWithHeaders(
- new ProfileActivated($event->id()),
+ new ProfileActivated($event->id()),
$message->headers()
);
return [$profileRegisteredMessage, $profileActivatedMessage];
- }
+ }
}
```
@@ -406,4 +404,4 @@ final class SplitProfileCreatedMiddleware implements Middleware
!!! note
- You can find more about messages [here](event_bus.md).
\ No newline at end of file
+ You can find more about messages [here](event_bus.md).
diff --git a/docs/pages/processor.md b/docs/pages/processor.md
index 5d8d39dd..51e1dccd 100644
--- a/docs/pages/processor.md
+++ b/docs/pages/processor.md
@@ -21,7 +21,7 @@ final class SendEmailProcessor implements Listener
public function __invoke(Message $message): void
{
$event = $message->event();
-
+
if (!$event instanceof ProfileCreated) {
return;
}
@@ -37,14 +37,13 @@ final class SendEmailProcessor implements Listener
!!! warning
- If you only want to listen to certain events,
+ If you only want to listen to certain events,
then you have to check it in the `__invoke` method or use the subscriber.
!!! tip
You can find out more about the event bus [here](event_bus.md).
-
## Subscriber
You can also create the whole thing as a subscriber too.
diff --git a/docs/pages/projection.md b/docs/pages/projection.md
index 1a3bddff..d544bd93 100644
--- a/docs/pages/projection.md
+++ b/docs/pages/projection.md
@@ -27,11 +27,11 @@ final class ProfileProjection implements Projector
private readonly Connection $connection
) {
}
-
+
/**
* @return list
*/
- public function getProfiles(): array
+ public function getProfiles(): array
{
return $this->connection->fetchAllAssociative('SELECT id, name FROM projection_profile;');
}
@@ -52,7 +52,7 @@ final class ProfileProjection implements Projector
public function handleProfileCreated(Message $message): void
{
$profileCreated = $message->event();
-
+
$this->connection->executeStatement(
'INSERT INTO projection_profile (`id`, `name`) VALUES(:id, :name);',
[
@@ -78,12 +78,12 @@ Several projectors can also listen to the same event.
!!! danger
- You should not execute any actions with projectors,
+ You should not execute any actions with projectors,
otherwise these will be executed again if you rebuild the projection!
!!! tip
- If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin)
+ If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin)
to make the event method return the correct type.
## Projector Repository
@@ -157,4 +157,4 @@ $eventBus->addListener(
!!! note
- In order to exploit the full potential, the [projectionist](./projectionist.md) should be used in production.
\ No newline at end of file
+ In order to exploit the full potential, the [projectionist](./projectionist.md) should be used in production.
diff --git a/docs/pages/projectionist.md b/docs/pages/projectionist.md
index 5cc32e2e..cf8a552c 100644
--- a/docs/pages/projectionist.md
+++ b/docs/pages/projectionist.md
@@ -1,8 +1,8 @@
# Projectionist
The projectionist manages individual projectors and keeps the projections running.
-Internally, the projectionist does this by tracking where each projection is in the event stream
-and keeping all projections up to date.
+Internally, the projectionist does this by tracking where each projection is in the event stream
+and keeping all projections up to date.
He also takes care that new projections are booted and old ones are removed again.
If something breaks, the projectionist marks the individual projections as faulty.
@@ -19,12 +19,12 @@ If something breaks, the projectionist marks the individual projections as fault
## Versioned Projector
-So that the projectionist can also assign the projectors to a projection,
-the interface must be changed from `Projector` to `VersionedProjector`.
+So that the projectionist can also assign the projectors to a projection,
+the interface must be changed from `Projector` to `VersionedProjector`.
As the name suggests, the projector will also be versionable.
-In addition, you have to implement the `targetProjection` method, which returns the target projection id.
-So that several versions of the projection can exist,
+In addition, you have to implement the `targetProjection` method, which returns the target projection id.
+So that several versions of the projection can exist,
the version of the projection should flow into the table or collection name.
```php
@@ -42,19 +42,19 @@ final class ProfileProjection implements VersionedProjector
private readonly Connection $connection
) {
}
-
- public function targetProjection(): ProjectionId
+
+ public function targetProjection(): ProjectionId
{
return new ProjectionId(
- name: 'profile',
+ name: 'profile',
version: 1
);
}
-
+
/**
* @return list
*/
- public function getProfiles(): array
+ public function getProfiles(): array
{
return $this->connection->fetchAllAssociative(
sprintf('SELECT id, name FROM %s;', $this->table())
@@ -66,7 +66,7 @@ final class ProfileProjection implements VersionedProjector
{
$this->connection->executeStatement(
sprintf(
- 'CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);',
+ 'CREATE TABLE IF NOT EXISTS %s (id VARCHAR PRIMARY KEY, name VARCHAR NOT NULL);',
$this->table()
)
);
@@ -84,7 +84,7 @@ final class ProfileProjection implements VersionedProjector
public function handleProfileCreated(Message $message): void
{
$profileCreated = $message->event();
-
+
$this->connection->executeStatement(
sprintf('INSERT INTO %s (`id`, `name`) VALUES(:id, :name);', $this->table()),
[
@@ -93,12 +93,12 @@ final class ProfileProjection implements VersionedProjector
]
);
}
-
- private function table(): string
+
+ private function table(): string
{
return sprintf(
- 'projection_%s_%s',
- $this->targetProjection()->name(),
+ 'projection_%s_%s',
+ $this->targetProjection()->name(),
$this->targetProjection()->version()
);
}
@@ -118,10 +118,10 @@ So that the projectionist knows where the projection stopped and must continue.
## Projection Status
-There is a lifecycle for each projection.
+There is a lifecycle for each projection.
This cycle is tracked by the projectionist.
-``` mermaid
+```mermaid
stateDiagram-v2
direction LR
[*] --> New
@@ -155,29 +155,29 @@ These projections have a projector, follow the event stream and are up to date.
### Outdated
-If a projection exists in the projection store
-that does not have a projector in the source code with a corresponding projection id,
+If a projection exists in the projection store
+that does not have a projector in the source code with a corresponding projection id,
then this projection is marked as outdated.
This happens when either the projector has been deleted
or the projection id of a projector has changed.
In the last case there is a new projection.
-An outdated projection does not automatically become active again when the projection id exists again.
+An outdated projection does not automatically become active again when the projection id exists again.
This happens, for example, when an old version was deployed again during a rollback.
There are two options here:
-* Reactivate the projection.
-* Remove the projection and rebuild it from scratch.
+- Reactivate the projection.
+- Remove the projection and rebuild it from scratch.
### Error
-If an error occurs in a projector, then the target projection is set to Error.
-This projection will then no longer run until the projection is activated again.
+If an error occurs in a projector, then the target projection is set to Error.
+This projection will then no longer run until the projection is activated again.
There are two options here:
-* Reactivate the projection.
-* Remove the projection and rebuild it from scratch.
+- Reactivate the projection.
+- Remove the projection and rebuild it from scratch.
## Setup
@@ -187,7 +187,6 @@ In order for the projectionist to be able to do its work, you have to assemble i
The SyncProjectorListener must be removed again so that the events are not processed directly!
-
### Projection Store
In order for the projectionist to know the status and position of the projections, they must be saved.
@@ -200,7 +199,7 @@ use Patchlevel\EventSourcing\Projection\Projection\Store\DoctrineStore;
$projectionStore = new DoctrineStore($connection);
```
-So that the schema for the projection store can also be created,
+So that the schema for the projection store can also be created,
we have to tell the `SchemaDirector` our schema configuration.
Using `ChainSchemaConfigurator` we can add multiple schema configurators.
In our case they need the `SchemaConfigurator` from the event store and projection store.
@@ -220,7 +219,7 @@ $schemaDirector = new DoctrineSchemaDirector(
!!! note
- You can find more about schema configurator [here](./store.md)
+ You can find more about schema configurator [here](./store.md)
### Projectionist
@@ -242,7 +241,7 @@ The Projectionist has a few methods needed to use it effectively. These are expl
### Boot
-So that the projectionist can manage the projections, they must be booted.
+So that the projectionist can manage the projections, they must be booted.
In this step, the structures are created for all new projections.
The projections then catch up with the current position of the event stream.
When the projections are finished, they switch to the active state.
@@ -261,7 +260,7 @@ $projectionist->run();
### Teardown
-If projections are outdated, they can be cleaned up here.
+If projections are outdated, they can be cleaned up here.
The projectionist also tries to remove the structures created for the projection.
```php
@@ -279,7 +278,7 @@ $projectionist->remove();
### Reactivate
-If a projection had an error, you can reactivate it.
+If a projection had an error, you can reactivate it.
As a result, the projection gets the status active again and is then kept up-to-date again by the projectionist.
```php
diff --git a/docs/pages/repository.md b/docs/pages/repository.md
index 076d6097..722f3a96 100644
--- a/docs/pages/repository.md
+++ b/docs/pages/repository.md
@@ -3,7 +3,7 @@
A `repository` takes care of storing and loading the `aggregates`.
He is also responsible for building [messages](event_bus.md) from the events and then dispatching them to the event bus.
-Every aggregate needs a repository to be stored.
+Every aggregate needs a repository to be stored.
And each repository is only responsible for one aggregate.
## Create a repository
@@ -11,9 +11,9 @@ And each repository is only responsible for one aggregate.
The best way to create a repository is to use the `DefaultRepositoryManager`.
This helps to build the repository correctly.
-The `DefaultRepositoryManager` needs some services to work.
-For one, it needs [AggregateRootRegistry](aggregate.md#aggregate-root-registry) so that it knows which aggregates exist.
-The [store](store.md), which is then given to the repository so that it can save and load the events at the end.
+The `DefaultRepositoryManager` needs some services to work.
+For one, it needs [AggregateRootRegistry](aggregate.md#aggregate-root-registry) so that it knows which aggregates exist.
+The [store](store.md), which is then given to the repository so that it can save and load the events at the end.
And the [EventBus](event_bus.md) to publish the new events.
After plugging the `DefaultRepositoryManager` together, you can create the repository associated with the aggregate.
@@ -36,7 +36,7 @@ $repository = $repositoryManager->get(Profile::class);
### Snapshots
-Loading events for an aggregate is superfast.
+Loading events for an aggregate is superfast.
You can have thousands of events in the database that load in a few milliseconds and build the corresponding aggregate.
But at some point you realize that it takes time. To counteract this there is a snapshot store.
@@ -88,8 +88,8 @@ $repository = $repositoryManager->get(Profile::class);
!!! warning
- We also use the decorator to fill in the `recordedOn` time.
- If you want to add your own decorator, then you need to make sure to add the `RecordedOnDecorator` as well.
+ We also use the decorator to fill in the `recordedOn` time.
+ If you want to add your own decorator, then you need to make sure to add the `RecordedOnDecorator` as well.
You can e.g. solve with the `ChainMessageDecorator`.
!!! note
@@ -98,15 +98,15 @@ $repository = $repositoryManager->get(Profile::class);
## Use the repository
-Each `repository` has three methods that are responsible for loading an `aggregate`,
+Each `repository` has three methods that are responsible for loading an `aggregate`,
saving it or checking whether it exists.
### Save an aggregate
-An `aggregate` can be `saved`.
-All new events that have not yet been written to the database are fetched from the aggregate.
-These events are then also append to the database.
-After the events have been written,
+An `aggregate` can be `saved`.
+All new events that have not yet been written to the database are fetched from the aggregate.
+These events are then also append to the database.
+After the events have been written,
the new events are dispatched on the [event bus](./event_bus.md).
```php
@@ -121,12 +121,12 @@ $repository->save($profile);
!!! tip
- If you want to make sure that dispatching events and storing events is transaction safe,
+ If you want to make sure that dispatching events and storing events is transaction safe,
then you should look at the [outbox](outbox.md) pattern.
### Load an aggregate
-An `aggregate` can be loaded using the `load` method.
+An `aggregate` can be loaded using the `load` method.
All events for the aggregate are loaded from the database and the current state is rebuilt.
```php
@@ -139,12 +139,12 @@ $profile = $repository->load('229286ff-6f95-4df6-bc72-0a239fe7b284');
!!! note
- You can only fetch one aggregate at a time and don't do any complex queries either.
+ You can only fetch one aggregate at a time and don't do any complex queries either.
Projections are used for this purpose.
### Has an aggregate
-You can also check whether an `aggregate` with a certain id exists.
+You can also check whether an `aggregate` with a certain id exists.
It is checked whether any event with this id exists in the database.
```php
@@ -155,7 +155,7 @@ if($repository->has('229286ff-6f95-4df6-bc72-0a239fe7b284')) {
!!! note
- The query is fast and does not load any event.
+ The query is fast and does not load any event.
This means that the state of the aggregate is not rebuild either.
## Custom Repository
@@ -172,27 +172,27 @@ This also gives you more type security.
use Patchlevel\EventSourcing\Repository\Repository;
use Patchlevel\EventSourcing\Repository\RepositoryManager;
-class ProfileRepository
+class ProfileRepository
{
/** @var Repository */
private Repository $repository;
- public function __construct(RepositoryManager $repositoryManager)
+ public function __construct(RepositoryManager $repositoryManager)
{
$this->repository = $repositoryManager->get(Profile::class);
}
-
- public function load(ProfileId $id): Profile
+
+ public function load(ProfileId $id): Profile
{
return $this->repository->load($id->toString());
}
-
- public function save(Profile $profile): void
+
+ public function save(Profile $profile): void
{
return $this->repository->save($profile);
}
-
- public function has(ProfileId $id): bool
+
+ public function has(ProfileId $id): bool
{
return $this->repository->has($id->toString());
}
diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md
index 6bd532eb..4d2db58e 100644
--- a/docs/pages/snapshots.md
+++ b/docs/pages/snapshots.md
@@ -1,21 +1,21 @@
# Snapshots
-Some aggregates can have a large number of events.
-This is not a problem if there are a few hundred.
-But if the number gets bigger at some point, then loading and rebuilding can become slow.
+Some aggregates can have a large number of events.
+This is not a problem if there are a few hundred.
+But if the number gets bigger at some point, then loading and rebuilding can become slow.
The `snapshot` system can be used to control this.
-Normally, the events are all executed again on the aggregate in order to rebuild the current state.
+Normally, the events are all executed again on the aggregate in order to rebuild the current state.
With a `snapshot`, we can shorten the way in which we temporarily save the current state of the aggregate.
-When loading it is checked whether the snapshot exists.
-If a hit exists, the aggregate is built up with the help of the snapshot.
-A check is then made to see whether further events have existed since the snapshot
-and these are then also executed on the aggregate.
+When loading it is checked whether the snapshot exists.
+If a hit exists, the aggregate is built up with the help of the snapshot.
+A check is then made to see whether further events have existed since the snapshot
+and these are then also executed on the aggregate.
Here, however, only the last events are loaded from the database and not all.
## Configuration
-First of all you have to define a snapshot store. This store may have multiple adapters for different caches.
+First of all you have to define a snapshot store. This store may have multiple adapters for different caches.
These caches also need a name so that you can determine which aggregates should be stored in which cache.
```php
@@ -48,7 +48,7 @@ $repositoryManager = new DefaultRepositoryManager(
You can read more about Repository [here](./repository.md).
-Next we need to tell the Aggregate to take a snapshot of it. We do this using the snapshot attribute.
+Next we need to tell the Aggregate to take a snapshot of it. We do this using the snapshot attribute.
There we also specify where it should be saved.
```php
@@ -64,10 +64,10 @@ final class Profile extends AggregateRoot
}
```
-When taking a snapshot, all properties are extracted and saved.
-When loading, this data is written back to the properties.
-In other words, in the end everything has to be serializable.
-To ensure this, the same system is used as for the events.
+When taking a snapshot, all properties are extracted and saved.
+When loading, this data is written back to the properties.
+In other words, in the end everything has to be serializable.
+To ensure this, the same system is used as for the events.
You can define normalizers to bring the properties into the correct format.
```php
@@ -83,7 +83,7 @@ final class Profile extends AggregateRoot
public string $name,
#[Normalize(new DateTimeImmutableNormalizer())]
public DateTimeImmutable $createdAt;
-
+
// ...
}
```
@@ -103,9 +103,9 @@ final class Profile extends AggregateRoot
### Snapshot batching
-Since the loading of events in itself is quite fast and only becomes noticeably slower with thousands of events,
-we do not need to create a snapshot after each event. That would also have a negative impact on performance.
-Instead, we can also create a snapshot after `N` events.
+Since the loading of events in itself is quite fast and only becomes noticeably slower with thousands of events,
+we do not need to create a snapshot after each event. That would also have a negative impact on performance.
+Instead, we can also create a snapshot after `N` events.
The remaining events that are not in the snapshot are then loaded from store.
```php
@@ -123,11 +123,11 @@ final class Profile extends AggregateRoot
### Snapshot versioning
-Whenever something changes on the aggregate, the previous snapshot must be discarded.
-You can do this by removing the entire snapshot cache when deploying.
-But that can be quickly forgotten. It is much easier to specify a snapshot version.
-This snapshot version is also saved. When loading, the versions are compared and if they do not match,
-the snapshot is discarded and the aggregate is rebuilt from scratch.
+Whenever something changes on the aggregate, the previous snapshot must be discarded.
+You can do this by removing the entire snapshot cache when deploying.
+But that can be quickly forgotten. It is much easier to specify a snapshot version.
+This snapshot version is also saved. When loading, the versions are compared and if they do not match,
+the snapshot is discarded and the aggregate is rebuilt from scratch.
The new aggregate is then saved again as a snapshot.
```php
@@ -154,15 +154,15 @@ final class Profile extends AggregateRoot
## Adapter
We offer a few `SnapshotAdapter` implementations that you can use.
-But not a direct implementation of a cache.
-There are many good libraries out there that address this problem,
-and before we reinvent the wheel, choose one of them.
-Since there is a psr-6 and psr-16 standard, there are plenty of libraries.
+But not a direct implementation of a cache.
+There are many good libraries out there that address this problem,
+and before we reinvent the wheel, choose one of them.
+Since there is a psr-6 and psr-16 standard, there are plenty of libraries.
Here are a few listed:
-* [symfony cache](https://symfony.com/doc/current/components/cache.html)
-* [laminas cache](https://docs.laminas.dev/laminas-cache/)
-* [scrapbook](https://www.scrapbook.cash/)
+- [symfony cache](https://symfony.com/doc/current/components/cache.html)
+- [laminas cache](https://docs.laminas.dev/laminas-cache/)
+- [scrapbook](https://www.scrapbook.cash/)
### psr6
diff --git a/docs/pages/split_stream.md b/docs/pages/split_stream.md
index eab13635..8254eb71 100644
--- a/docs/pages/split_stream.md
+++ b/docs/pages/split_stream.md
@@ -6,14 +6,14 @@ says if the user start a new subscription all past events should not be consider
banking scenario. There the business decides to save the current state every quarter for each banking account.
Not only that some businesses requires such an action it also increases the performance for aggregate which would have a
-really long event stream.
+really long event stream.
## Flagging an event to split the stream
To use this feature you need to add the `SplitStreamDecorator`. You will also need events which will trigger this
-action. For that you can use the `#[SplitStream]` attribute. We decided that we are not literallty splitting the stream,
-instead we are marking all past events as archived as soon as this event is saved. Then the past events will not be
-loaded anymore for building the aggregate. This means that all needed data has to be present in these events which
+action. For that you can use the `#[SplitStream]` attribute. We decided that we are not literallty splitting the stream,
+instead we are marking all past events as archived as soon as this event is saved. Then the past events will not be
+loaded anymore for building the aggregate. This means that all needed data has to be present in these events which
should trigger the event split.
```php
@@ -33,7 +33,7 @@ final class MonthPassed
!!! warning
- The event needs all data which is relevant the aggregate to be used since all past event will not be loaded! Keep
+ The event needs all data which is relevant the aggregate to be used since all past event will not be loaded! Keep
this in mind if you want to use this feature.
!!! note
diff --git a/docs/pages/store.md b/docs/pages/store.md
index 9284d37a..c4d3aa31 100644
--- a/docs/pages/store.md
+++ b/docs/pages/store.md
@@ -20,7 +20,7 @@ $connection = DriverManager::getConnection([
!!! note
- You can find out more about how to create a connection
+ You can find out more about how to create a connection
[here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html)
## Store types
@@ -110,17 +110,17 @@ $store->transactionRollback();
### Transactional function
-There is also the possibility of executing a function in a transaction.
+There is also the possibility of executing a function in a transaction.
Then dbal takes care of starting a transaction, committing it and then possibly rollback it again.
```php
$store->transactional(function () use ($command, $bankAccountRepository) {
$accountFrom = $bankAccountRepository->get($command->from());
$accountTo = $bankAccountRepository->get($command->to());
-
+
$accountFrom->transferMoney($command->to(), $command->amount());
$accountTo->receiveMoney($command->from(), $command->amount());
-
+
$bankAccountRepository->save($accountFrom);
$bankAccountRepository->save($accountTo);
});
@@ -128,8 +128,8 @@ $store->transactional(function () use ($command, $bankAccountRepository) {
!!! tip
- To ensure that all listeners are executed for the released events
- or that the listeners are not executed if the transaction fails,
+ To ensure that all listeners are executed for the released events
+ or that the listeners are not executed if the transaction fails,
you can use the [outbox](outbox.md) pattern for it.
## Schema
diff --git a/docs/pages/stylesheets/extra.css b/docs/pages/stylesheets/extra.css
index 9cdd3ee3..f0d8da24 100644
--- a/docs/pages/stylesheets/extra.css
+++ b/docs/pages/stylesheets/extra.css
@@ -1,31 +1,31 @@
:root > * {
- --md-code-hl-number-color: #6897BB;
- --md-code-hl-special-color: #C7CDD7;
- --md-code-hl-function-color: #FFC66D;
- --md-code-hl-constant-color: #9876AA;
- --md-code-hl-keyword-color: #CC7832;
- --md-code-hl-string-color: #6A8759;
- --md-code-hl-name-color: red;
- --md-code-hl-operator-color: #C7CDD7;
- --md-code-hl-punctuation-color: #C7CDD7;
- --md-code-hl-comment-color: #629755;
- --md-code-hl-generic-color: red;
- --md-code-hl-variable-color: #9876AA;
+ --md-code-hl-number-color: #6897bb;
+ --md-code-hl-special-color: #c7cdd7;
+ --md-code-hl-function-color: #ffc66d;
+ --md-code-hl-constant-color: #9876aa;
+ --md-code-hl-keyword-color: #cc7832;
+ --md-code-hl-string-color: #6a8759;
+ --md-code-hl-name-color: red;
+ --md-code-hl-operator-color: #c7cdd7;
+ --md-code-hl-punctuation-color: #c7cdd7;
+ --md-code-hl-comment-color: #629755;
+ --md-code-hl-generic-color: red;
+ --md-code-hl-variable-color: #9876aa;
- --md-code-fg-color: #C7CDD7;
- --md-code-bg-color: #2B2B2B;
- --md-code-hl-color: red;
+ --md-code-fg-color: #c7cdd7;
+ --md-code-bg-color: #2b2b2b;
+ --md-code-hl-color: red;
}
p {
- margin-block-start: 2em;
- margin-block-end: 2em;
+ margin-block-start: 2em;
+ margin-block-end: 2em;
}
.md-typeset code {
- padding: .2em .4em;
+ padding: 0.2em 0.4em;
}
.admonition-title {
- margin-bottom: -1em !important;
-}
\ No newline at end of file
+ margin-bottom: -1em !important;
+}
diff --git a/docs/pages/tests.md b/docs/pages/tests.md
index 36b34a95..148055ab 100644
--- a/docs/pages/tests.md
+++ b/docs/pages/tests.md
@@ -1,7 +1,7 @@
# Tests
-The aggregates can also be tested very well.
-You can test whether certain events have been thrown
+The aggregates can also be tested very well.
+You can test whether certain events have been thrown
or whether the state is set up correctly when the aggregate is set up again via the events.
```php
@@ -15,29 +15,29 @@ final class ProfileTest extends TestCase
$profile = Profile::createProfile($id, Email::fromString('foo@email.com'));
self::assertEquals(
- $profile->releaseEvents(),
+ $profile->releaseEvents(),
[
- new ProfileCreated($id, Email::fromString('foo@email.com')),
+ new ProfileCreated($id, Email::fromString('foo@email.com')),
]
);
self::assertEquals('foo@email.com', $profile->email()->toString());
}
-
+
public function testChangeName(): void
{
$id = ProfileId::generate();
-
+
$profile = Profile::createFromEvents([
new ProfileCreated($id, Email::fromString('foo@email.com')),
]);
-
+
$profile->changeEmail(Email::fromString('bar@email.com'));
-
+
self::assertEquals(
- $profile->releaseEvents(),
+ $profile->releaseEvents(),
[
- new EmailChanged(Email::fromString('bar@email.com')),
+ new EmailChanged(Email::fromString('bar@email.com')),
]
);
diff --git a/docs/pages/upcasting.md b/docs/pages/upcasting.md
index 7d7a32f9..2dfce4e0 100644
--- a/docs/pages/upcasting.md
+++ b/docs/pages/upcasting.md
@@ -23,7 +23,7 @@ final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster
if ($upcast->eventName !== 'profile_created') {
return $upcast;
}
-
+
return $upcast->replacePayloadByKey('email', strtolower($upcast->payload['email']);
}
}
@@ -49,7 +49,7 @@ final class LegacyEventNameUpaster implements Upcaster
public function __construct(
private readonly EventRegistry $eventRegistry
){}
-
+
public function __invoke(Upcast $upcast): Upcast
{
return $upcast->replaceEventName(
@@ -61,7 +61,7 @@ final class LegacyEventNameUpaster implements Upcaster
## Use upcasting
-After we have defined the upcasting rules, we also have to pass the whole thing to the serializer.
+After we have defined the upcasting rules, we also have to pass the whole thing to the serializer.
Since we have multiple upcasters, we use a chain here.
```php
@@ -92,8 +92,8 @@ final class EventStreamCleanupCommand extends Command
protected static $defaultDescription = 'rebuild event stream';
public function __construct(
- private readonly Store $sourceStore,
- private readonly Store $targetStore,
+ private readonly Store $sourceStore,
+ private readonly Store $targetStore,
private readonly ProjectionHandler $projectionHandler
){
}
@@ -101,17 +101,17 @@ final class EventStreamCleanupCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int
{
$pipeline = new Pipeline(
- new StoreSource($sourceStore),
+ new StoreSource($sourceStore),
new StoreTarget($targetStore)
);
-
+
$pipeline->run();
}
```
!!! danger
- Under no circumstances may the same store be used that is used for the source.
+ Under no circumstances may the same store be used that is used for the source.
Otherwise the store will be broken afterwards!
!!! note
diff --git a/docs/pages/uuid.md b/docs/pages/uuid.md
index e8323e86..85b08710 100644
--- a/docs/pages/uuid.md
+++ b/docs/pages/uuid.md
@@ -2,8 +2,8 @@
A UUID can be generated for the `aggregateId`. There are two popular libraries that can be used:
-* [ramsey/uuid](https://github.com/ramsey/uuid)
-* [symfony/uid](https://symfony.com/doc/current/components/uid.html)
+- [ramsey/uuid](https://github.com/ramsey/uuid)
+- [symfony/uid](https://symfony.com/doc/current/components/uid.html)
The `aggregate` does not care how the id is generated, since only an aggregate-wide unique string is expected here.
@@ -24,13 +24,13 @@ final class Profile extends AggregateRoot
{
return $this->id->toString();
}
-
- public function id(): UuidInterface
+
+ public function id(): UuidInterface
{
return $this->id;
}
-
- public function name(): string
+
+ public function name(): string
{
return $this->name;
}
@@ -38,15 +38,15 @@ final class Profile extends AggregateRoot
public static function create(string $name): self
{
$id = Uuid::uuid4();
-
+
$self = new self();
$self->recordThat(new ProfileCreated($id, $name));
return $self;
}
-
+
#[Apply]
- protected function applyProfileCreated(ProfileCreated $event): void
+ protected function applyProfileCreated(ProfileCreated $event): void
{
$this->id = $event->profileId();
$this->name = $event->name();
@@ -61,21 +61,21 @@ The whole thing looks like this:
```php
use Ramsey\Uuid\Uuid;
-class ProfileId
+class ProfileId
{
private string $id;
-
- public function __construct(string $id)
+
+ public function __construct(string $id)
{
$this->id = $id;
}
-
- public static function generate(): self
+
+ public static function generate(): self
{
return new self(Uuid::uuid4()->toString());
}
-
- public function toString(): string
+
+ public function toString(): string
{
return $this->id;
}
@@ -99,13 +99,13 @@ final class Profile extends AggregateRoot
{
return $this->id->toString();
}
-
- public function id(): ProfileId
+
+ public function id(): ProfileId
{
return $this->id;
}
-
- public function name(): string
+
+ public function name(): string
{
return $this->name;
}
@@ -113,15 +113,15 @@ final class Profile extends AggregateRoot
public static function create(string $name): self
{
$id = ProfileId::generate();
-
+
$self = new self();
$self->recordThat(new ProfileCreated($id, $name));
return $self;
}
-
+
#[Apply]
- protected function applyProfileCreated(ProfileCreated $event): void
+ protected function applyProfileCreated(ProfileCreated $event): void
{
$this->id = $event->profileId();
$this->name = $event->name();
@@ -131,5 +131,5 @@ final class Profile extends AggregateRoot
!!! note
- If you want to use snapshots, then you have to make sure that the value objects are normalized.
+ 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).