diff --git a/baseline.xml b/baseline.xml
index 1a315c922..552bc8ad4 100644
--- a/baseline.xml
+++ b/baseline.xml
@@ -92,6 +92,7 @@
$bus
+ $id$repository$store
@@ -100,14 +101,19 @@
$adapter$bus
+ $id$repository$snapshotStore$store
+
+ id]]>
+ $bus
+ $id$repository$snapshotStore$store
@@ -116,6 +122,7 @@
$bus
+ $id$profile$repository$store
@@ -187,15 +194,36 @@
$id
+
+
+ $id
+
+
+
+ $id
+
+
+
+ $id
+
+ $event
+
+ $id
+
+
+
+
+ $id
+
@@ -204,6 +232,16 @@
$messages
+
+
+ $id
+
+
+
+
+ $id
+
+ current
diff --git a/composer.json b/composer.json
index 90ba31fb4..0b48a17f5 100644
--- a/composer.json
+++ b/composer.json
@@ -27,6 +27,7 @@
"psr/clock": "^1.0",
"psr/log": "^2.0.0|^3.0.0",
"psr/simple-cache": "^2.0.0|^3.0.0",
+ "ramsey/uuid": "^4.7",
"symfony/console": "^5.4.32|^6.4.1|^7.0.1",
"symfony/finder": "^5.4.27|^6.4.0|^7.0.0",
"symfony/lock": "^5.4.32|^6.4.0|^7.0.0"
diff --git a/composer.lock b/composer.lock
index e668a73c5..3a6cde5af 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,63 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e4f404ab231a5bdafd3f79b6c0214ae7",
+ "content-hash": "6d8b289f331bdf12d6cd2dca8ef7ff36",
"packages": [
+ {
+ "name": "brick/math",
+ "version": "0.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/math.git",
+ "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478",
+ "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpunit/phpunit": "^9.0",
+ "vimeo/psalm": "5.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Arbitrary-precision arithmetic library",
+ "keywords": [
+ "Arbitrary-precision",
+ "BigInteger",
+ "BigRational",
+ "arithmetic",
+ "bigdecimal",
+ "bignum",
+ "brick",
+ "math"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/math/issues",
+ "source": "https://github.com/brick/math/tree/0.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2023-01-15T23:15:59+00:00"
+ },
{
"name": "doctrine/cache",
"version": "2.2.0",
@@ -778,6 +833,187 @@
},
"time": "2021-10-29T13:26:27+00:00"
},
+ {
+ "name": "ramsey/collection",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/collection.git",
+ "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
+ "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "captainhook/plugin-composer": "^5.3",
+ "ergebnis/composer-normalize": "^2.28.3",
+ "fakerphp/faker": "^1.21",
+ "hamcrest/hamcrest-php": "^2.0",
+ "jangregor/phpstan-prophecy": "^1.0",
+ "mockery/mockery": "^1.5",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3",
+ "phpcsstandards/phpcsutils": "^1.0.0-rc1",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpstan/extension-installer": "^1.2",
+ "phpstan/phpstan": "^1.9",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5",
+ "psalm/plugin-mockery": "^1.1",
+ "psalm/plugin-phpunit": "^0.18.4",
+ "ramsey/coding-standard": "^2.0.3",
+ "ramsey/conventional-commits": "^1.3",
+ "vimeo/psalm": "^5.4"
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ },
+ "ramsey/conventional-commits": {
+ "configFile": "conventional-commits.json"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Ramsey\\Collection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ben Ramsey",
+ "email": "ben@benramsey.com",
+ "homepage": "https://benramsey.com"
+ }
+ ],
+ "description": "A PHP library for representing and manipulating collections.",
+ "keywords": [
+ "array",
+ "collection",
+ "hash",
+ "map",
+ "queue",
+ "set"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/collection/issues",
+ "source": "https://github.com/ramsey/collection/tree/2.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ramsey",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-31T21:50:55+00:00"
+ },
+ {
+ "name": "ramsey/uuid",
+ "version": "4.7.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/uuid.git",
+ "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e",
+ "reference": "5f0df49ae5ad6efb7afa69e6bfab4e5b1e080d8e",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11",
+ "ext-json": "*",
+ "php": "^8.0",
+ "ramsey/collection": "^1.2 || ^2.0"
+ },
+ "replace": {
+ "rhumsaa/uuid": "self.version"
+ },
+ "require-dev": {
+ "captainhook/captainhook": "^5.10",
+ "captainhook/plugin-composer": "^5.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+ "doctrine/annotations": "^1.8",
+ "ergebnis/composer-normalize": "^2.15",
+ "mockery/mockery": "^1.3",
+ "paragonie/random-lib": "^2",
+ "php-mock/php-mock": "^2.2",
+ "php-mock/php-mock-mockery": "^1.3",
+ "php-parallel-lint/php-parallel-lint": "^1.1",
+ "phpbench/phpbench": "^1.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9",
+ "ramsey/composer-repl": "^1.4",
+ "slevomat/coding-standard": "^8.4",
+ "squizlabs/php_codesniffer": "^3.5",
+ "vimeo/psalm": "^4.9"
+ },
+ "suggest": {
+ "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+ "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+ "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
+ "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+ "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Ramsey\\Uuid\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
+ "keywords": [
+ "guid",
+ "identifier",
+ "uuid"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/uuid/issues",
+ "source": "https://github.com/ramsey/uuid/tree/4.7.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ramsey",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-11-08T05:53:05+00:00"
+ },
{
"name": "symfony/console",
"version": "v7.0.1",
diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md
index 3e27093fd..cc4ae5ff9 100644
--- a/docs/pages/aggregate.md
+++ b/docs/pages/aggregate.md
@@ -1,38 +1,44 @@
# Aggregate
-!!! abstract
+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.
- 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. [...]
+!!! note
+
+ The term aggregate itself comes from DDD and has nothing to do with event sourcing and can be used independently as a pattern.
+ You can find out more about Aggregates [here](https://martinfowler.com/bliki/DDD_Aggregate.html).
- [DDD Aggregate - Martin Flower](https://martinfowler.com/bliki/DDD_Aggregate.html)
+An aggregate must fulfill a few points so that we can use it in event-sourcing:
-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.
-The events will be added later, but the following is enough to make it executable:
+* It must implement the `AggregateRoot` interface.
+* It needs a unique identifier.
+* It needs to provide the current playhead.
+* It must make changes to his state available as events.
+* And rebuild/catchup its state from the events.
-To register an aggregate you have to set the `Aggregate` attribute over the class,
-otherwise it will not be recognized as an aggregate.
-There you also have to give the aggregate a name.
+We can implement this ourselves, or use the `BasicAggregateRoot` implementation that already brings everything with it.
+This basic implementation uses attributes to configure the aggregate and to specify how it should handle events.
+We are building a minimal aggregate class here which only has an ID and mark this with the `Id` attribute.
+To make it easy to register with a name, we also add the `Aggregate` attribute. This is what it looks like:
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
- public function aggregateRootId(): string
- {
- return $this->id;
- }
-
- public static function register(string $id): self
+ public static function register(Uuid $id): self
{
$self = new self();
- // todo: record create event
+
+ $self->id = $id; // we need to set the id temporary here for the basic example and will be replaced later.
return $self;
}
@@ -45,7 +51,7 @@ final class Profile extends BasicAggregateRoot
!!! tip
- An aggregateId can be an **uuid**, you can find more about this [here](./uuid.md).
+ Find out more about aggregate IDs [here](./aggregate_id.md).
We use a so-called named constructor here to create an object of the AggregateRoot.
The constructor itself is protected and cannot be called from outside.
@@ -77,7 +83,7 @@ final class CreateProfileHandler
This is because only events are stored in the database and as long as no events exist,
nothing happens.
-!!! note
+!!! tip
A **command bus** system is not necessary, only recommended.
The interaction can also easily take place in a controller or service.
@@ -85,16 +91,22 @@ final class CreateProfileHandler
## Create a new aggregate
In order that an aggregate is actually saved, at least one event must exist in the DB.
-For our aggregate we create the Event `ProfileRegistered`:
+For our aggregate we create the Event `ProfileRegistered` with an ID and a name.
+Since the ID is a complex data type and cannot be easily serialized, we need to define a normalizer for the ID.
+We do this with the `IdNormalizer` attribute.
+We also give the event a unique name using the `Event` attribute.
```php
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Event;
+use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
#[Event('profile.registered')]
final class ProfileRegistered
{
public function __construct(
- public readonly string $profileId,
+ #[IdNormalizer(Uuid::class)]
+ public readonly Uuid $profileId,
public readonly string $name
) {}
}
@@ -104,30 +116,28 @@ final class ProfileRegistered
You can find out more about events [here](./events.md).
-After we have defined the event, we have to adapt the creation of the profile:
+After we have defined the event, we have to adapt the profile aggregate:
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private string $name;
-
- public function aggregateRootId(): string
- {
- return $this->id;
- }
public function name(): string
{
return $this->name;
}
- public static function register(string $id, string $name): self
+ public static function register(Uuid $id, string $name): self
{
$self = new self();
$self->recordThat(new ProfileRegistered($id, $name));
@@ -150,12 +160,15 @@ final class Profile extends BasicAggregateRoot
In our named constructor `register` we have now created the event and recorded it with the method `recordThat`.
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.
+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,
-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.
+we have to mark it with the `Apply` attribute. We did that in the `applyProfileRegistered` method.
+In there we then change the state of the aggregate by filling the properties with the values from the event.
+
+!!! success
+
+ The aggregate is now ready to be saved!
### Modify an aggregate
@@ -184,26 +197,24 @@ This method then creates the event `NameChanged` and records it:
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private string $name;
- public function aggregateRootId(): string
- {
- return $this->id;
- }
-
public function name(): string
{
return $this->name;
}
- public static function register(string $id, string $name): static
+ public static function register(Uuid $id, string $name): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name));
@@ -287,9 +298,6 @@ use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
- private string $name;
-
// ...
#[Apply(ProfileCreated::class)]
@@ -322,9 +330,6 @@ use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
#[SuppressMissingApply([NameChanged::class])]
final class Profile extends BasicAggregateRoot
{
- private string $id;
- private string $name;
-
// ...
#[Apply]
@@ -350,9 +355,6 @@ use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
#[SuppressMissingApply(SuppressMissingApply::ALL)]
final class Profile extends BasicAggregateRoot
{
- private string $id;
- private string $name;
-
// ...
#[Apply]
@@ -388,16 +390,8 @@ use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
- private string $name;
-
// ...
- public function name(): string
- {
- return $this->name;
- }
-
public function changeName(string $name): void
{
if (strlen($name) < 3) {
@@ -417,7 +411,7 @@ final class Profile extends BasicAggregateRoot
!!! danger
- Disregarding this can break the rebuilding of the state!
+ Validations during "apply" can brake the rebuilding of the aggregate.
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.
@@ -450,16 +444,19 @@ We can now use the value object `Name` in our aggregate:
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private Name $name;
- public static function register(string $id, Name $name): static
+ public static function register(Uuid $id, Name $name): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name));
@@ -566,17 +563,20 @@ But you can pass this information by yourself.
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private Name $name;
private DateTimeImmutable $registeredAt;
- public static function register(string $id, string $name, DateTimeImmutable $registeredAt): static
+ public static function register(Uuid $id, string $name, DateTimeImmutable $registeredAt): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name, $registeredAt));
@@ -592,18 +592,21 @@ But if you still want to make sure that the time is "now" and not in the past or
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Clock\Clock;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private Name $name;
private DateTimeImmutable $registeredAt;
- public static function register(string $id, string $name, Clock $clock): static
+ public static function register(Uuid $id, string $name, Clock $clock): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name, $clock->now()));
diff --git a/docs/pages/uuid.md b/docs/pages/aggregate_id.md
similarity index 100%
rename from docs/pages/uuid.md
rename to docs/pages/aggregate_id.md
diff --git a/docs/pages/events.md b/docs/pages/events.md
index ee3a8045a..5a3f8690e 100644
--- a/docs/pages/events.md
+++ b/docs/pages/events.md
@@ -67,14 +67,18 @@ so that the library knows how to write this data to the database and load it aga
```php
use Patchlevel\EventSourcing\Attribute\Event;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
+use Patchlevel\Hydrator\Normalizer\IdNormalizer;
-#[Event('profile.name_changed')]
-final class NameChanged
+#[Event('profile.created')]
+final class ProfileCreated
{
public function __construct(
- public readonly string $name,
+ #[IdNormalizer(Uuid::class)]
+ public readonly Uuid $id,
+ #[NameNormalizer]
+ public readonly Name $name,
#[DateTimeImmutableNormalizer]
- public readonly DateTimeImmutable $changedAt
+ public readonly DateTimeImmutable $createdAt
) {}
}
```
diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md
index 53e5f0fff..07ed91b37 100644
--- a/docs/pages/getting_started.md
+++ b/docs/pages/getting_started.md
@@ -10,11 +10,15 @@ First we define the events that happen in our system.
A hotel can be created with a `name` and a `id`:
```php
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
+
#[Event('hotel.created')]
final class HotelCreated
{
public function __construct(
- public readonly string $hotelId,
+ #[IdNormalizer(Uuid::class)]
+ public readonly Uuid $hotelId,
public readonly string $hotelName
) {
}
@@ -60,13 +64,16 @@ These events are thrown here and the state of the hotel is also changed.
```php
use Patchlevel\EventSourcing\Aggregate\AggregateChanged;
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Apply;
#[Aggregate('hotel')]
final class Hotel extends BasicAggregateRoot
{
- private string $id;
+ #[Id]
+ private Uuid $id;
private string $name;
/**
@@ -84,7 +91,7 @@ final class Hotel extends BasicAggregateRoot
return $this->guests;
}
- public static function create(string $id, string $hotelName): static
+ public static function create(Uuid $id, string $hotelName): static
{
$self = new static();
$self->recordThat(new HotelCreated($id, $hotelName));
@@ -134,11 +141,6 @@ final class Hotel extends BasicAggregateRoot
)
);
}
-
- public function aggregateRootId(): string
- {
- return $this->id;
- }
}
```
@@ -188,7 +190,7 @@ final class HotelProjector
$this->db->insert(
$this->table(),
[
- 'id' => $event->hotelId,
+ 'id' => $message->aggregateId(),
'name' => $event->hotelName,
'guests' => 0
]
@@ -364,14 +366,16 @@ $projectionist->boot();
We are now ready to use the Event Sourcing System. We can load, change and save aggregates.
```php
-$hotel1 = Hotel::create('1', 'HOTEL');
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+
+$hotel1 = Hotel::create(Uuid::v7(), 'HOTEL');
$hotel1->checkIn('David');
$hotel1->checkIn('Daniel');
$hotel1->checkOut('David');
$hotelRepository->save($hotel1);
-$hotel2 = $hotelRepository->load('2');
+$hotel2 = $hotelRepository->load(Uuid::fromString('d0d0d0d0-d0d0-d0d0-d0d0-d0d0d0d0d0d0'));
$hotel2->checkIn('David');
$hotelRepository->save($hotel2);
@@ -380,7 +384,8 @@ $hotels = $hotelProjection->getHotels();
!!! note
- An aggregateId can be an **uuid**, you can find more about this [here](uuid.md).
+ You can also use other forms of IDs such as uuid version 6 or a custom format.
+ You can find more about this [here](aggregate_id.md).
## Result
diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md
index 6d7924c4f..2cd8b5e0c 100644
--- a/docs/pages/normalizer.md
+++ b/docs/pages/normalizer.md
@@ -200,6 +200,21 @@ final class DTO {
}
```
+### Id
+
+If you have your own AggregateRootId, you can use the `IdNormalizer`.
+the `IdNormalizer` needs the FQCN of the AggregateRootId as a parameter.
+
+```php
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\Hydrator\Normalizer\IdNormalizer;
+
+final class DTO {
+ #[IdNormalizer(Uuid::class)]
+ public Uuid $id;
+}
+```
+
## Custom Normalizer
Since we only offer normalizers for PHP native things,
@@ -235,6 +250,7 @@ Finally, you have to allow the normalizer to be used as an attribute.
```php
use Patchlevel\Hydrator\Normalizer\Normalizer;
+use Patchlevel\Hydrator\Normalizer\InvalidArgument;
#[Attribute(Attribute::TARGET_PROPERTY)]
class NameNormalizer implements Normalizer
@@ -242,7 +258,7 @@ class NameNormalizer implements Normalizer
public function normalize(mixed $value): string
{
if (!$value instanceof Name) {
- throw new InvalidArgumentException();
+ throw InvalidArgument::withWrongType(Name::class, $value);
}
return $value->toString();
@@ -255,7 +271,7 @@ class NameNormalizer implements Normalizer
}
if (!is_string($value)) {
- throw new InvalidArgumentException();
+ throw InvalidArgument::withWrongType('string', $value);
}
return new Name($value);
diff --git a/docs/pages/pipeline.md b/docs/pages/pipeline.md
index 7c7c207b7..96af29bd4 100644
--- a/docs/pages/pipeline.md
+++ b/docs/pages/pipeline.md
@@ -111,7 +111,7 @@ $source = new class implements Source {
public function count(): int
{
- reutrn 1;
+ return 1;
}
}
```
diff --git a/docs/pages/repository.md b/docs/pages/repository.md
index 75c7bd19f..dd3adf485 100644
--- a/docs/pages/repository.md
+++ b/docs/pages/repository.md
@@ -67,13 +67,12 @@ $repository = $repositoryManager->get(Profile::class);
### Decorator
-If you want to add more metadata to the message, like e.g. an application id, then you can use decorator.
+If you want to add more metadata to the message, like e.g. an application id, then you can use decorators.
```php
-use Patchlevel\EventSourcing\EventBus\Decorator\RecordedOnDecorator;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
-$decorator = new RecordedOnDecorator($clock);
+$decorator = new ApplicationIdDecorator();
$repositoryManager = new DefaultRepositoryManager(
$aggregateRootRegistry,
@@ -104,7 +103,10 @@ After the events have been written,
the new events are dispatched on the [event bus](./event_bus.md).
```php
-$profile = Profile::create('david.badura@patchlevel.de');
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+
+$id = Uuid::v7();
+$profile = Profile::create($id, 'david.badura@patchlevel.de');
$repository->save($profile);
```
@@ -124,12 +126,15 @@ 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
-$profile = $repository->load('229286ff-6f95-4df6-bc72-0a239fe7b284');
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+
+$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
+$profile = $repository->load($id);
```
!!! warning
- When the method is called, the aggregate is always reloaded from the database and rebuilt.
+ When the method is called, the aggregate is always reloaded and rebuilt from the database.
!!! note
@@ -142,7 +147,9 @@ 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
-if($repository->has('229286ff-6f95-4df6-bc72-0a239fe7b284')) {
+$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
+
+if($repository->has($id)) {
// ...
}
```
@@ -178,7 +185,7 @@ class ProfileRepository
public function load(ProfileId $id): Profile
{
- return $this->repository->load($id->toString());
+ return $this->repository->load($id);
}
public function save(Profile $profile): void
@@ -188,7 +195,7 @@ class ProfileRepository
public function has(ProfileId $id): bool
{
- return $this->repository->has($id->toString());
+ return $this->repository->has($id);
}
}
```
diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md
index a9ed4a0e2..fc33c026e 100644
--- a/docs/pages/snapshots.md
+++ b/docs/pages/snapshots.md
@@ -72,14 +72,19 @@ You can define normalizers to bring the properties into the correct format.
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Snapshot;
+use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
#[Aggregate('profile')]
#[Snapshot('default')]
final class Profile extends BasicAggregateRoot
{
- public string $id;
+ #[Id]
+ #[IdNormalizer]
+ public Uuid $id;
public string $name,
#[Normalize(new DateTimeImmutableNormalizer())]
public DateTimeImmutable $createdAt;
@@ -95,7 +100,7 @@ final class Profile extends BasicAggregateRoot
!!! warning
- In the end it has to be possible to serialize it as json.
+ In the end it the complete aggregate must be serializeable as json, also the aggregate Id.
!!! note
@@ -193,3 +198,41 @@ use Patchlevel\EventSourcing\Snapshot\Adapter\InMemorySnapshotAdapter;
$adapter = new InMemorySnapshotAdapter();
```
+
+## Usage
+
+The snapshot store is automatically used by the repository and takes care of saving and loading.
+But you can also use the snapshot store yourself.
+
+### Save
+
+This allows you to save the aggregate as a snapshot:
+
+```php
+$snapshotStore->save($aggregate);
+```
+
+!!! danger
+
+ If the state of an aggregate is saved as a snapshot without being saved to the event store (database),
+ it can lead to data loss or broken aggregates!
+
+### Load
+
+You can also load an aggregate from the snapshot store:
+
+```php
+use Patchlevel\EventSourcing\Aggregate\Uuid;
+
+$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
+$aggregate = $snapshotStore->load(Profile::class, $id);
+```
+
+The method returns the Aggregate if it was loaded successfully.
+If the aggregate was not found, then a `SnapshotNotFound` is thrown.
+And if the version is no longer correct and the snapshot is therefore invalid, then a `SnapshotVersionInvalid` is thrown.
+
+!!! warning
+
+ The aggregate may be in an old state as the snapshot may lag behind.
+ You still have to bring the aggregate up to date by loading the missing events from the event store.
\ No newline at end of file
diff --git a/src/Aggregate/AggregateRoot.php b/src/Aggregate/AggregateRoot.php
index c7682b639..84c6eabcf 100644
--- a/src/Aggregate/AggregateRoot.php
+++ b/src/Aggregate/AggregateRoot.php
@@ -6,7 +6,7 @@
interface AggregateRoot
{
- public function aggregateRootId(): string;
+ public function aggregateRootId(): AggregateRootId;
/** @param iterable