diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md
deleted file mode 100644
index 938d230c0..000000000
--- a/UPGRADE-2.0.md
+++ /dev/null
@@ -1,402 +0,0 @@
-# Upgrading to v2
-
-We are only providing here the most important changes in detail. For the full BC-Break list go to
-the [full BC-Break list](#Full-BC-Break-list)
-
-## Detailed change list
-
-### Events
-
-* Removed `AggregateChanged` abstract class. Use `#[Event('eventName')]` instead.
-
-Before:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\AggregateChanged;
-
-final class ProfileCreated extends AggregateChanged
-{
- public static function raise(string $id, string $name): static
- {
- return new static($id, ['profileId' => $id, 'name' => $name]);
- }
-
- public function profileId(): string
- {
- return $this->aggregateId;
- }
-
- public function name(): string
- {
- return $this->payload['name'];
- }
-}
-```
-
-After:
-
-```php
-#[Event('profile.created')]
-final class ProfileCreated
-{
- public function __construct(
- public readonly string $profileId,
- public readonly string $name
- )
- {
- }
-}
-```
-
-* Added `#[Normalize(new YourNormalizer())]` for more complicated properties like ValueObjects.
-* Added `#[NormalizedName('foo')]`.
-
-```php
-use YourApp\EmailNormalizer;
-use Patchlevel\EventSourcing\Attribute\Normalize;
-
-#[Event('profile.created')]
-final class ProfileCreated
-{
- public function __construct(
- public readonly string $profileId,
- public readonly string $name,
- #[Normalize(new EmailNormalizer())]
- public readonly Email $email,
- )
- {
- }
-}
-```
-
-### Aggregates
-
-* Aggregates now needs an attribute `#[Aggregate('name')]` with a unique name.
-* Removed `AttributeApplyMethod` trait. Is default behaviour now.
-* Removed `StrictApplyMethod` trait. Is default behaviour now.
-* Removed `NonStrictApplyMethod` trait. Use `#[SuppressMissingApply]` instead.
-* Rename method `record` to `recordThat`.
-
-### Schema
-
-* Changed database column from `aggregateId` to `aggregate_id`.
-* Changed database column from `recordedOn` to `recorded_on`.
-* Changed value of database column `event` from FQCN of the event to the name provided via the attribute.
-
-Update the two columns, for single table store:
-
-```SQL
-ALTER TABLE eventstore RENAME COLUMN aggregateId TO aggregate_id;
-ALTER TABLE eventstore RENAME COLUMN recordedOn TO recorded_on;
-```
-
-For the event name change we recommend using the upcasting feature like this:
-
-```php
-use Patchlevel\EventSourcing\Metadata\Event\EventRegistry;
-use Patchlevel\EventSourcing\Serializer\Upcast\Upcaster;
-use Patchlevel\EventSourcing\Serializer\Upcast\Upcast;
-
-final class LegacyEventNameUpaster implements Upcaster
-{
- public function __construct(private readonly EventRegistry $eventRegistry){}
-
- public function __invoke(Upcast $upcast): Upcast
- {
- return new Upcast($this->eventRegistry->eventName($upcast->eventName), $upcast->payload);
- }
-}
-```
-
-If you dont want to upcast everytime you could also use the pipeline to recreate the event-stream with the fixed event
-name. See for that the documentation.
-
-### Store & Pipeline
-
-* Renamed and adjusted parameters for `save` at `Store`
-* Removed `saveBatch` at `Store`. Use new `save` instead.
-* Renamed and adjusted parameters for `save` at `PipelineStore`
-* Removed `saveBatch` at `PipelineStore`. Use new `save` instead.
-* Changed parameter for `Middleware::__invoke` from `EventBucket` to `Message`.
-* Removed `EventBucket`. `Message` is kinda a replacement.
-* Removed `ClassRenameMiddleware`. This is not needed anymore since the event name is saved instead.
-* Removed `FromIndexEventMiddleware`.
-
-### Projection
-
-* Removed `create` method from `Projection` interface. Use `#[Create]` instead.
-* Removed `drop` method from `Projection` interface. Use `#[Drop]` instead.
-* Removed `handledEvents` method from `Projection` interface. Use `#[Handle]` instead.
-* Rename `ProjectionRepository` to `ProjectionHandler`.
-* Rename `DefaultProjectionRepistory` to `DefaultProjectionHandler`.
-
-### EventBus
-
-* Changed `DefaultEventBus::dispatch` to accept multiple `Message` objects instead of one `AggregateChanged`.
-
-### Snapshot
-
-* Removed `SnapshotableAggregateRoot` abstract class. Use `#[Snapshot]` instead.
-
-### Clock
-
-* Removed `Clock` singleton class. See `SystemClock`, `FrozenClock` as an implementation of the `Clock` interface
- instead.
-
-## Full BC-Break list
-
-#### Added
-
-- [BC] Method save() was added to interface Patchlevel\EventSourcing\Store\PipelineStore
-- [BC] Method save() was added to interface Patchlevel\EventSourcing\Store\Store
-
-#### Changed
-
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\EventBus#dispatch() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\EventBus#dispatch() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to Patchlevel\EventSourcing\EventBus\Message
-- [BC] Parameter 0 of Patchlevel\EventSourcing\EventBus\EventBus#dispatch() changed name from event to messages
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\Listener#__invoke() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\Listener#__invoke() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to Patchlevel\EventSourcing\EventBus\Message
-- [BC] Parameter 0 of Patchlevel\EventSourcing\EventBus\Listener#__invoke() changed name from event to message
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\SymfonyEventBus#dispatch() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $event of Patchlevel\EventSourcing\EventBus\DefaultEventBus#dispatch() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $event of Patchlevel\EventSourcing\WatchServer\WatchServerClient#send() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $event of Patchlevel\EventSourcing\WatchServer\WatchServerClient#send() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to Patchlevel\EventSourcing\EventBus\Message
-- [BC] Parameter 0 of Patchlevel\EventSourcing\WatchServer\WatchServerClient#send() changed name from event to message
-- [BC] The parameter $event of Patchlevel\EventSourcing\WatchServer\WatchListener#__invoke() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $snapshot of Patchlevel\EventSourcing\Snapshot\SnapshotStore#save() changed from
- Patchlevel\EventSourcing\Snapshot\Snapshot to a non-contravariant Patchlevel\EventSourcing\Aggregate\AggregateRoot
-- [BC] The parameter $snapshot of Patchlevel\EventSourcing\Snapshot\SnapshotStore#save() changed from
- Patchlevel\EventSourcing\Snapshot\Snapshot to Patchlevel\EventSourcing\Aggregate\AggregateRoot
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Snapshot\SnapshotStore#save() changed name from snapshot to aggregateRoot
-- [BC] The return type of Patchlevel\EventSourcing\Snapshot\SnapshotStore#load() changed from
- Patchlevel\EventSourcing\Snapshot\Snapshot to the non-covariant Patchlevel\EventSourcing\Aggregate\AggregateRoot
-- [BC] The return type of Patchlevel\EventSourcing\Snapshot\SnapshotStore#load() changed from
- Patchlevel\EventSourcing\Snapshot\Snapshot to Patchlevel\EventSourcing\Aggregate\AggregateRoot
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Snapshot\SnapshotStore#load() changed name from aggregate to
- aggregateClass
-- [BC] Class Patchlevel\EventSourcing\Attribute\Apply became final
-- [BC] The parameter $aggregateChangedClass of Patchlevel\EventSourcing\Attribute\Apply#__construct() changed from
- string to string|null
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Attribute\Apply#__construct() changed name from aggregateChangedClass to
- eventClass
-- [BC] Class Patchlevel\EventSourcing\Attribute\SuppressMissingApply became final
-- [BC] Class Patchlevel\EventSourcing\Attribute\Handle became final
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Attribute\Handle#__construct() changed name from aggregateChangedClass to
- eventClass
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Target\Target#save() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Target\Target#save() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to Patchlevel\EventSourcing\EventBus\Message
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Pipeline\Target\Target#save() changed name from bucket to message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget#save() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Target\StoreTarget#save() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Target\ProjectionTarget#save() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\UntilEventMiddleware#__invoke() changed
- from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\ChainMiddleware#__invoke() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\ExcludeEventMiddleware#__invoke() changed
- from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware#__invoke()
- changed from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant
- Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\Middleware#__invoke() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\Middleware#__invoke() changed from
- Patchlevel\EventSourcing\Pipeline\EventBucket to Patchlevel\EventSourcing\EventBus\Message
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Pipeline\Middleware\Middleware#__invoke() changed name from bucket to
- message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\IncludeEventMiddleware#__invoke() changed
- from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware#__invoke() changed
- from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $bucket of Patchlevel\EventSourcing\Pipeline\Middleware\FilterEventMiddleware#__invoke() changed
- from Patchlevel\EventSourcing\Pipeline\EventBucket to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-- [BC] The parameter $aggregate of Patchlevel\EventSourcing\Aggregate\ApplyMethodNotFound#__construct() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateRoot to a non-contravariant string
-- [BC] The parameter $event of Patchlevel\EventSourcing\Aggregate\ApplyMethodNotFound#__construct() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant string
-- [BC] The parameter $event of Patchlevel\EventSourcing\Aggregate\AggregateRoot#apply() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to object
-- [BC] The number of required arguments for Patchlevel\EventSourcing\Store\DoctrineStore#__construct() increased from 1
- to 3
-- [BC] Parameter 0 of Patchlevel\EventSourcing\Store\DoctrineStore#__construct() changed name from eventConnection to
- connection
-- [BC] The parameter $aggregates of Patchlevel\EventSourcing\Store\SingleTableStore#__construct() changed from array to
- a non-contravariant Patchlevel\EventSourcing\Serializer\EventSerializer
-- [BC] The parameter $storeTableName of Patchlevel\EventSourcing\Store\SingleTableStore#__construct() changed from
- string to a non-contravariant Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry
-- [BC] The number of required arguments for Patchlevel\EventSourcing\Store\MultiTableStore#__construct() increased from
- 2 to 3
-- [BC] The parameter $aggregates of Patchlevel\EventSourcing\Store\MultiTableStore#__construct() changed from array to a
- non-contravariant Patchlevel\EventSourcing\Serializer\EventSerializer
-- [BC] The parameter $metadataTableName of Patchlevel\EventSourcing\Store\MultiTableStore#__construct() changed from
- string to a non-contravariant Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $projectionRepository of Patchlevel\EventSourcing\Console\Command\ProjectionRebuildCommand#__
- construct() changed from Patchlevel\EventSourcing\Projection\ProjectionRepository to a non-contravariant
- Patchlevel\EventSourcing\Projection\ProjectionHandler
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The number of required arguments for Patchlevel\EventSourcing\Console\Command\WatchCommand#__construct()
- increased from 1 to 2
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $projectionRepository of Patchlevel\EventSourcing\Console\Command\ProjectionCreateCommand#__
- construct() changed from Patchlevel\EventSourcing\Projection\ProjectionRepository to a non-contravariant
- Patchlevel\EventSourcing\Projection\ProjectionHandler
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The number of required arguments for Patchlevel\EventSourcing\Console\Command\ShowCommand#__construct() increased
- from 2 to 3
-- [BC] The parameter $aggregates of Patchlevel\EventSourcing\Console\Command\ShowCommand#__construct() changed from
- array to a non-contravariant Patchlevel\EventSourcing\Serializer\EventSerializer
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $projectionRepository of Patchlevel\EventSourcing\Console\Command\ProjectionDropCommand#__
- construct() changed from Patchlevel\EventSourcing\Projection\ProjectionRepository to a non-contravariant
- Patchlevel\EventSourcing\Projection\ProjectionHandler
-- [BC] The parameter $definition of Symfony\Component\Console\Command\Command#setDefinition() changed from no type to a
- non-contravariant array|Symfony\Component\Console\Input\InputDefinition
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addArgument() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $shortcut of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant string|array|null
-- [BC] The parameter $default of Symfony\Component\Console\Command\Command#addOption() changed from no type to a
- non-contravariant mixed|null
-- [BC] The parameter $repository of Patchlevel\EventSourcing\Projection\ProjectionListener#__construct() changed from
- Patchlevel\EventSourcing\Projection\ProjectionRepository to a non-contravariant
- Patchlevel\EventSourcing\Projection\ProjectionHandler
-- [BC] The parameter $event of Patchlevel\EventSourcing\Projection\ProjectionListener#__invoke() changed from
- Patchlevel\EventSourcing\Aggregate\AggregateChanged to a non-contravariant Patchlevel\EventSourcing\EventBus\Message
-
-#### Removed
-
-- [BC] Class Patchlevel\EventSourcing\Clock has been deleted
-- [BC] Class Patchlevel\EventSourcing\WatchServer\DefaultWatchServerClient has been deleted
-- [BC] Class Patchlevel\EventSourcing\WatchServer\DefaultWatchServer has been deleted
-- [BC] Class Patchlevel\EventSourcing\Repository\SnapshotRepository has been deleted
-- [BC] Class Patchlevel\EventSourcing\Snapshot\Psr6SnapshotStore has been deleted
-- [BC] Class Patchlevel\EventSourcing\Snapshot\Psr16SnapshotStore has been deleted
-- [BC] Class Patchlevel\EventSourcing\Snapshot\BatchSnapshotStore has been deleted
-- [BC] Class Patchlevel\EventSourcing\Snapshot\InMemorySnapshotStore has been deleted
-- [BC] Method Patchlevel\EventSourcing\Attribute\Apply#aggregateChangedClass() was removed
-- [BC] Method Patchlevel\EventSourcing\Attribute\Handle#aggregateChangedClass() was removed
-- [BC] Class Patchlevel\EventSourcing\Pipeline\Target\ProjectionRepositoryTarget has been deleted
-- [BC] Method Patchlevel\EventSourcing\Pipeline\Target\InMemoryTarget#buckets() was removed
-- [BC] Class Patchlevel\EventSourcing\Pipeline\EventBucket has been deleted
-- [BC] Class Patchlevel\EventSourcing\Pipeline\Middleware\ClassRenameMiddleware has been deleted
-- [BC] Method Patchlevel\EventSourcing\Pipeline\Middleware\RecalculatePlayheadMiddleware#__construct() was removed
-- [BC] Class Patchlevel\EventSourcing\Pipeline\Middleware\FromIndexEventMiddleware has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\StrictApplyMethod has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\AttributeApplyMethod has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\AggregateChangeRecordedAlready has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\DuplicateApplyMethod has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\AggregateChanged has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\SnapshotableAggregateRoot has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\ClockRecordDate has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\NonStrictApplyMethod has been deleted
-- [BC] Method Patchlevel\EventSourcing\Aggregate\AggregateRoot#record() was removed
-- [BC] Method Patchlevel\EventSourcing\Aggregate\AggregateRoot::createFromEventStream() was removed
-- [BC] Class Patchlevel\EventSourcing\Aggregate\AggregateChangeNotRecorded has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\PlayheadSequenceMismatch has been deleted
-- [BC] Class Patchlevel\EventSourcing\Aggregate\ApplyAttributeNotFound has been deleted
-- [BC] Method Patchlevel\EventSourcing\Store\PipelineStore#saveEventBucket() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\Store#saveBatch() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\DoctrineStore::normalizeResult() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\Store#saveBatch() was removed
-- [BC] Class Patchlevel\EventSourcing\Store\AggregateIdMismatch has been deleted
-- [BC] Method Patchlevel\EventSourcing\Store\Store#saveBatch() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\SingleTableStore#saveBatch() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\SingleTableStore#saveEventBucket() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\DoctrineStore::normalizeResult() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\MultiTableStore#saveBatch() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\MultiTableStore#saveEventBucket() was removed
-- [BC] Method Patchlevel\EventSourcing\Store\DoctrineStore::normalizeResult() was removed
-- [BC] Class Patchlevel\EventSourcing\Store\AggregateNotDefined has been deleted
-- [BC] Class Patchlevel\EventSourcing\Console\EventPrinter has been deleted
-- [BC] Method Patchlevel\EventSourcing\Projection\Projection#handledEvents() was removed
-- [BC] Method Patchlevel\EventSourcing\Projection\Projection#create() was removed
-- [BC] Method Patchlevel\EventSourcing\Projection\Projection#drop() was removed
-- [BC] Class Patchlevel\EventSourcing\Projection\AttributeHandleMethod has been deleted
-- [BC] Class Patchlevel\EventSourcing\Projection\MethodDoesNotExist has been deleted
-- [BC] Class Patchlevel\EventSourcing\Projection\ProjectionRepository has been deleted
-- [BC] Class Patchlevel\EventSourcing\Projection\DefaultProjectionRepository has been deleted
-- [BC] Class Patchlevel\EventSourcing\Projection\DuplicateHandleMethod has been deleted
-- [BC] Class Patchlevel\EventSourcing\Projection\ProjectionException has been deleted
diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md
deleted file mode 100644
index 3793a144e..000000000
--- a/UPGRADE-3.0.md
+++ /dev/null
@@ -1,2 +0,0 @@
-The upgrade path is located in our docs. Please have a look at
-[here](https://patchlevel.github.io/event-sourcing-docs/3.0/UPGRADE-3.0/).
diff --git a/baseline.xml b/baseline.xml
index e11f9c79d..d51023299 100644
--- a/baseline.xml
+++ b/baseline.xml
@@ -99,15 +99,9 @@
-
-
-
-
-
-
+
+ getClassName()]]>
+
diff --git a/deptrac-baseline.yaml b/deptrac-baseline.yaml
index fe42e99fd..ed8398130 100644
--- a/deptrac-baseline.yaml
+++ b/deptrac-baseline.yaml
@@ -1,11 +1,5 @@
deptrac:
skip_violations:
- Patchlevel\EventSourcing\Aggregate\AggregateRootId:
- - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
- Patchlevel\EventSourcing\Aggregate\CustomId:
- - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
- Patchlevel\EventSourcing\Aggregate\Uuid:
- - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
Patchlevel\EventSourcing\Attribute\Processor:
- Patchlevel\EventSourcing\Subscription\RunMode
Patchlevel\EventSourcing\Attribute\Projector:
@@ -14,3 +8,9 @@ deptrac:
- Patchlevel\EventSourcing\Aggregate\AggregateRoot
Patchlevel\EventSourcing\Attribute\Subscriber:
- Patchlevel\EventSourcing\Subscription\RunMode
+ Patchlevel\EventSourcing\Identifier\CustomId:
+ - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
+ Patchlevel\EventSourcing\Identifier\Identifier:
+ - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
+ Patchlevel\EventSourcing\Identifier\Uuid:
+ - Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer
diff --git a/deptrac.yaml b/deptrac.yaml
index ded380b15..c785f07f3 100644
--- a/deptrac.yaml
+++ b/deptrac.yaml
@@ -33,6 +33,10 @@ deptrac:
collectors:
- type: directory
value: src/EventBus/.*
+ - name: Identifier
+ collectors:
+ - type: directory
+ value: src/Identifier/.*
- name: Message
collectors:
- type: directory
@@ -104,12 +108,14 @@ deptrac:
ruleset:
Aggregate:
- Attribute
+ - Identifier
- MetadataAggregate
Attribute:
Clock:
CommandBus:
- Aggregate
- Attribute
+ - Identifier
- MetadataAggregate
- Repository
Console:
@@ -159,6 +165,7 @@ deptrac:
- Aggregate
- Attribute
- Clock
+ - Identifier
- Message
- MetadataEvent
- MetadataSubscriber
@@ -170,6 +177,7 @@ deptrac:
Repository:
- Aggregate
- Clock
+ - Identifier
- Message
- MetadataAggregate
- MetadataEvent
@@ -178,12 +186,13 @@ deptrac:
- Store
Schema:
Serializer:
- - Aggregate
- Cryptography
+ - Identifier
- MetadataEvent
Snapshot:
- Aggregate
- Cryptography
+ - Identifier
- MetadataAggregate
Store:
- Aggregate
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index eb2dadbb1..342d3913a 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -101,7 +101,7 @@ nav:
- Event Bus: event_bus.md
- Query Bus: query_bus.md
- Advanced:
- - Aggregate ID: aggregate_id.md
+ - Identifier: identifier.md
- Normalizer: normalizer.md
- Snapshots: snapshots.md
- Personal Data: personal_data.md
diff --git a/docs/pages/UPGRADE-4.0.md b/docs/pages/UPGRADE-4.0.md
index 52a4254fc..5b9abffff 100644
--- a/docs/pages/UPGRADE-4.0.md
+++ b/docs/pages/UPGRADE-4.0.md
@@ -2,12 +2,34 @@
## Aggregates
+### Aggregate Root
+
+Method `aggregateRootId` return typehint has been changed from `AggregateRootId` to `Identifier`.
+
+### Aggregate Root Id
+
+`AggregateRootId` was renamed to `Identifier` and moved to the `Patchlevel\EventSourcing\Identifier` namespace.
+
+Following classes have been moved to the `Patchlevel\EventSourcing\Identifier` namespace too:
+
+* `CustomId`
+* `CustomIdBehaviour`
+* `RamseyUuidV7Behaviour`
+* `Uuid`
+
+Return typehint of `fromString` method has been changed from `self` to `static`.
+All typehints of other classes `AggregateRootId` have been changed to `Identifier`.
+
### Child Aggregate
We removed our experimental feature of child aggregates.
This was our first attempt to split aggregates into smaller parts,
but we found a better way to do this with the `Micro Aggregate` feature.
+## Aggregate Repository
+
+Typehints for the `AggregateRepository` have been changed, from `AggregateRootId` to `Identifier`.
+
## Subscription
The constructor of the `DefaultSubscriptionEngine` class has been changed.
@@ -61,7 +83,6 @@ $subscriptionEngine = new DefaultSubscriptionEngine(
RetryStrategyRepository::withDefault($retryStrategy),
);
```
-
## Store
### DoctrineDbalStore
@@ -95,4 +116,4 @@ and replaced with the following headers:
### DoctrineSchemaSubscriber
The `Patchlevel\EventSourcing\Schema\DoctrineSchemaSubscriber` has been removed.
-use the `Patchlevel\EventSourcing\Schema\DoctrineSchemaListener` instead.
\ No newline at end of file
+use the `Patchlevel\EventSourcing\Schema\DoctrineSchemaListener` instead.
diff --git a/docs/pages/aggregate.md b/docs/pages/aggregate.md
index f9af65142..43132c272 100644
--- a/docs/pages/aggregate.md
+++ b/docs/pages/aggregate.md
@@ -24,9 +24,9 @@ To make it easy to register with a name, we also add the `Aggregate` attribute.
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
@@ -50,7 +50,7 @@ final class Profile extends BasicAggregateRoot
!!! tip
- Find out more about aggregate IDs [here](./aggregate_id.md).
+ Find out more about aggregate IDs [here](./identifier.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.
@@ -94,8 +94,8 @@ For our aggregate we create the Event `ProfileRegistered` with an ID and a name.
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\Identifier\Uuid;
#[Event('profile.registered')]
final class ProfileRegistered
@@ -115,10 +115,10 @@ 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\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
@@ -190,10 +190,10 @@ 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\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
@@ -466,10 +466,10 @@ 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\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
@@ -587,9 +587,9 @@ 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\Identifier\Uuid;
#[Aggregate('profile')]
final class Profile extends BasicAggregateRoot
@@ -614,9 +614,9 @@ 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\Identifier\Uuid;
use Psr\Clock\ClockInterface;
#[Aggregate('profile')]
@@ -663,10 +663,10 @@ The order handle the order itself and the shipping handle the shipping of the or
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('order')]
final class Order extends BasicAggregateRoot
@@ -694,11 +694,11 @@ In this case, the `Shipping` aggregate listens to the `OrderCreated` event to in
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\Stream;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('shipping')]
#[Stream(Order::class)]
@@ -756,7 +756,7 @@ $aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create([/* p
```
## Learn more
-* [How to create own aggregate id](aggregate_id.md)
+* [How to create own aggregate id](identifier.md)
* [How to store and load aggregates](repository.md)
* [How to snapshot aggregates](snapshots.md)
* [How to create Projections](subscription.md)
diff --git a/docs/pages/aggregate_id.md b/docs/pages/aggregate_id.md
deleted file mode 100644
index c4c336be4..000000000
--- a/docs/pages/aggregate_id.md
+++ /dev/null
@@ -1,148 +0,0 @@
-# Aggregate ID
-
-The `aggregate id` is a unique identifier for an aggregate.
-It is used to identify the aggregate in the event store.
-The `aggregate` does not care how the id is generated,
-since only an aggregate-wide unique string is expected in the store.
-
-This library provides you with a few options for generating the id.
-
-!!! warning
-
- Performance reasons, the default configuration of the store require an uuid string for `aggregate id`.
- But technically, for the library, it can be any string.
- If you want to use a custom id, you have to change the `aggregate_id_type` in the [store](store.md) configuration.
-
-## Uuid
-
-The easiest way is to use an `uuid` as an aggregate ID.
-For this, we have the `Uuid` class, which is a simple wrapper for the [ramsey/uuid](https://github.com/ramsey/uuid) library.
-
-You can use it like this:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\Uuid;
-use Patchlevel\EventSourcing\Attribute\Aggregate;
-use Patchlevel\EventSourcing\Attribute\Id;
-
-#[Aggregate('profile')]
-final class Profile extends BasicAggregateRoot
-{
- #[Id]
- private Uuid $id;
-}
-```
-You have multiple options for generating an uuid:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
-
-$uuid = Uuid::generate();
-$uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e');
-```
-!!! Note
-
- We implemented the version 7 of the uuid, because it is most suitable for event sourcing.
- More information about uuid versions can be found [here](https://uuid.ramsey.dev/en/stable/rfc4122.html).
-
-## Custom ID
-
-If you don't want to use an uuid, you can also use the custom ID implementation.
-This is a value object that holds any string.
-
-```php
-use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\CustomId;
-use Patchlevel\EventSourcing\Attribute\Aggregate;
-use Patchlevel\EventSourcing\Attribute\Id;
-
-#[Aggregate('profile')]
-final class Profile extends BasicAggregateRoot
-{
- #[Id]
- private CustomId $id;
-}
-```
-!!! warning
-
- If you want to use a custom id that is not an uuid,
- you need to change the `aggregate_id_type` to `string` in the store configuration.
- More information can be found [here](store.md).
-
-So you can use any string as an id:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\CustomId;
-
-$id = CustomId::fromString('my-id');
-```
-## Implement own ID
-
-Or even better, you create your own aggregate-specific ID class.
-This allows you to ensure that the correct id is always used.
-The whole thing looks like this:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\AggregateRootId;
-
-class ProfileId implements AggregateRootId
-{
- private function __construct(
- private readonly string $id,
- ) {
- }
-
- public function toString(): string
- {
- return $this->id;
- }
-
- public static function fromString(string $id): self
- {
- return new self($id);
- }
-}
-```
-So you can use it like this:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Attribute\Aggregate;
-use Patchlevel\EventSourcing\Attribute\Id;
-
-#[Aggregate('profile')]
-final class Profile extends BasicAggregateRoot
-{
- #[Id]
- private ProfileId $id;
-}
-```
-We also offer you some traits, so that you don't have to implement the `AggregateRootId` interface yourself.
-Here for the uuid:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\AggregateRootId;
-use Patchlevel\EventSourcing\Aggregate\RamseyUuidV7Behaviour;
-
-class ProfileId implements AggregateRootId
-{
- use RamseyUuidV7Behaviour;
-}
-```
-Or for the custom id:
-
-```php
-use Patchlevel\EventSourcing\Aggregate\AggregateRootId;
-use Patchlevel\EventSourcing\Aggregate\CustomIdBehaviour;
-
-class ProfileId implements AggregateRootId
-{
- use CustomIdBehaviour;
-}
-```
-### Learn more
-
-* [How to create an aggregate](aggregate.md)
-* [How to create an event](events.md)
-* [How to test an aggregate](testing.md)
diff --git a/docs/pages/command_bus.md b/docs/pages/command_bus.md
index 75d23da1c..33c2273f1 100644
--- a/docs/pages/command_bus.md
+++ b/docs/pages/command_bus.md
@@ -362,5 +362,5 @@ $provider = new ChainHandlerProvider([
* [How to use aggregates](aggregate.md)
* [How to use events](events.md)
* [How to use clock](clock.md)
-* [How to use aggregate id](aggregate_id.md)
+* [How to use aggregate id](identifier.md)
* [How to use query bus](query_bus.md)
diff --git a/docs/pages/events.md b/docs/pages/events.md
index 4541319fb..c2af1761a 100644
--- a/docs/pages/events.md
+++ b/docs/pages/events.md
@@ -89,8 +89,8 @@ 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
-use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Event;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;
diff --git a/docs/pages/getting_started.md b/docs/pages/getting_started.md
index 317ebcfae..3df5c7ad2 100644
--- a/docs/pages/getting_started.md
+++ b/docs/pages/getting_started.md
@@ -10,8 +10,8 @@ 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\Attribute\Event;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Event('hotel.created')]
final class HotelCreated
@@ -69,10 +69,10 @@ Last but not least, we need the associated apply methods to change the state.
```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
-use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
#[Aggregate('hotel')]
final class Hotel extends BasicAggregateRoot
@@ -373,7 +373,7 @@ $engine->setup(skipBooting: true);
We are now ready to use the Event Sourcing System. We can load, change and save aggregates.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Repository\Repository;
$hotel1 = Hotel::create(Uuid::generate(), 'HOTEL');
@@ -393,7 +393,7 @@ $hotels = $hotelProjection->getHotels();
!!! note
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).
+ You can find more about this [here](identifier.md).
## Result
diff --git a/docs/pages/identifier.md b/docs/pages/identifier.md
new file mode 100644
index 000000000..cc101b2d7
--- /dev/null
+++ b/docs/pages/identifier.md
@@ -0,0 +1,123 @@
+# Identifier
+
+Identifiers are small, immutable value objects that represent IDs across the library in a type‑safe and consistent way. They are primarily used for aggregate root IDs, but can also be applied anywhere a stable string identifier is required (snapshots, repositories, your own domain objects, etc.).
+
+All identifiers must implement `Patchlevel\EventSourcing\Identifier\Identifier` and provide a stable string representation for storage and serialization.
+
+## Interface
+
+```php
+interface Identifier
+{
+ public function toString(): string;
+
+ public static function fromString(string $id): static;
+}
+```
+The string value returned by `toString()` is what gets persisted (e.g. in the event store) and serialized. `fromString()` reconstructs the value object from that string.
+
+## Built‑in identifiers
+
+We provide two ready‑to‑use implementations:
+
+### `Uuid`
+
+`Uuid` wraps [ramsey/uuid](https://github.com/ramsey/uuid) and uses UUID v7 by default, which is a good fit for event‑sourcing.
+
+```php
+use Patchlevel\EventSourcing\Identifier\Uuid;
+
+$uuid = Uuid::generate();
+$uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e');
+```
+!!! note
+
+ UUID v7 provides k‑sortable identifiers that work well with append‑only streams and database indexes. See the ramsey docs for details.
+
+### `CustomId`
+
+`CustomId` is a minimal string‑backed identifier. Use it if you want full control over the string format or if your IDs are provided by an external system.
+
+```php
+use Patchlevel\EventSourcing\Identifier\CustomId;
+
+$id = CustomId::fromString('my-id');
+```
+## Domain‑specific identifiers
+
+For better domain modeling, define your own identifier types by implementing `Identifier` and encapsulating creation rules and validation. Two traits are available to make this easy:
+
+- `RamseyUuidV7Behaviour` for UUID v7 identifiers
+- `CustomIdBehaviour` for string‑backed identifiers
+
+```php
+use Patchlevel\EventSourcing\Identifier\Identifier;
+use Patchlevel\EventSourcing\Identifier\RamseyUuidV7Behaviour;
+
+final class ProfileId implements Identifier
+{
+ use RamseyUuidV7Behaviour;
+}
+```
+or
+
+```php
+use Patchlevel\EventSourcing\Identifier\CustomIdBehaviour;
+use Patchlevel\EventSourcing\Identifier\Identifier;
+
+final class OrderNumber implements Identifier
+{
+ use CustomIdBehaviour;
+}
+```
+## Using identifiers with aggregates
+
+Aggregates expose and persist their IDs as `Identifier` instances. You can choose the concrete implementation that fits your domain.
+
+```php
+use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
+use Patchlevel\EventSourcing\Identifier\Uuid;
+
+#[Aggregate('profile')]
+final class Profile extends BasicAggregateRoot
+{
+ #[Id]
+ private Uuid $id;
+}
+```
+Or use your domain‑specific identifier:
+
+```php
+use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
+use Patchlevel\EventSourcing\Attribute\Aggregate;
+use Patchlevel\EventSourcing\Attribute\Id;
+
+#[Aggregate('profile')]
+final class Profile extends BasicAggregateRoot
+{
+ #[Id]
+ private ProfileId $id;
+}
+```
+## Serialization and normalization
+
+Identifiers integrate with the serializer via `IdNormalizer`. The `Identifier` interface is annotated so that instances are automatically normalized to strings and denormalized back to the correct class.
+
+This means you can safely use identifier types in command payloads, events, or snapshots without writing custom normalizers.
+
+## Testing
+
+For deterministic tests involving UUIDs, use `FakeRamseyUuidFactory` to generate predictable UUID v7 values.
+
+## Notes
+
+- Identifiers are stored as strings in the backing store, but you should always use concrete identifier types in your domain code for type‑safety and clarity.
+- You can use identifiers beyond aggregates wherever an opaque, stable ID is needed.
+
+### Learn more
+
+* [How to create an aggregate](aggregate.md)
+* [How to create an event](events.md)
+* [How to test an aggregate](testing.md)
diff --git a/docs/pages/normalizer.md b/docs/pages/normalizer.md
index c798a0f22..c4fa7ed51 100644
--- a/docs/pages/normalizer.md
+++ b/docs/pages/normalizer.md
@@ -275,7 +275,7 @@ final class DTO
If you have your own AggregateRootId, you can use the `IdNormalizer`.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
final class DTO
@@ -291,7 +291,7 @@ final class DTO
Optional you can also define the type of the id.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
final class DTO
diff --git a/docs/pages/personal_data.md b/docs/pages/personal_data.md
index e89afffe2..fdfb9703f 100644
--- a/docs/pages/personal_data.md
+++ b/docs/pages/personal_data.md
@@ -26,7 +26,7 @@ In order for the correct key to be used, a subject ID must be defined.
Without Subject Id, no personal data can be encrypted or decrypted.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
final class EmailChanged
@@ -48,7 +48,7 @@ final class EmailChanged
Next, you have to mark the properties that should be encrypted with the `#[PersonalData]` attribute.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
diff --git a/docs/pages/repository.md b/docs/pages/repository.md
index cd0cd0a42..d66cb8302 100644
--- a/docs/pages/repository.md
+++ b/docs/pages/repository.md
@@ -154,7 +154,7 @@ All new events that have not yet been written to the database are fetched from t
These events are then also append to the database.
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Repository\Repository;
$id = Uuid::generate();
@@ -187,7 +187,7 @@ 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
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Repository\Repository;
$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
@@ -210,7 +210,7 @@ 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
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Repository\Repository;
$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
diff --git a/docs/pages/snapshots.md b/docs/pages/snapshots.md
index 30b9c1842..aa1d28e80 100644
--- a/docs/pages/snapshots.md
+++ b/docs/pages/snapshots.md
@@ -82,10 +82,10 @@ 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\Identifier\Uuid;
#[Aggregate('profile')]
#[Snapshot('default')]
@@ -239,7 +239,7 @@ $snapshotStore->save($aggregate);
You can also load an aggregate from the snapshot store:
```php
-use Patchlevel\EventSourcing\Aggregate\Uuid;
+use Patchlevel\EventSourcing\Identifier\Uuid;
use Patchlevel\EventSourcing\Snapshot\SnapshotStore;
$id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284');
diff --git a/docs/pages/testing.md b/docs/pages/testing.md
index d21f5d19a..aa8141aa5 100644
--- a/docs/pages/testing.md
+++ b/docs/pages/testing.md
@@ -198,7 +198,7 @@ Uuids are randomly generated and can be a problem in tests.
If you want deterministic tests, you can use the `IncrementalRamseyUuidFactory` from the library.
```php
-use Patchlevel\EventSourcing\Test\IncrementalRamseyUuidFactory;
+use Patchlevel\EventSourcing\Identifier\FakeRamseyUuidFactory;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
@@ -206,7 +206,7 @@ final class ProfileTest extends TestCase
{
public function setUp(): void
{
- Uuid::setFactory(new IncrementalRamseyUuidFactory());
+ Uuid::setFactory(new FakeRamseyUuidFactory());
}
public function testCreateProfile(): void
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 6d180b7c3..e656e2c60 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -48,6 +48,12 @@ parameters:
count: 1
path: src/Repository/DefaultRepository.php
+ -
+ message: '#^Property Patchlevel\\EventSourcing\\Serializer\\Normalizer\\IdNormalizer\:\:\$identifierClass \(class\-string\\|null\) does not accept string\.$#'
+ identifier: assign.propertyType
+ count: 1
+ path: src/Serializer/Normalizer/IdNormalizer.php
+
-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Hydrator\:\:hydrate\(\) expects array\, mixed given\.$#'
identifier: argument.type
@@ -138,12 +144,6 @@ parameters:
count: 3
path: src/Subscription/ThrowableToErrorContextTransformer.php
- -
- message: '#^Parameter \#1 \$id of static method Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\Profile\:\:create\(\) expects Patchlevel\\EventSourcing\\Tests\\Benchmark\\BasicImplementation\\ProfileId, Patchlevel\\EventSourcing\\Aggregate\\AggregateRootId given\.$#'
- identifier: argument.type
- count: 1
- path: tests/Benchmark/SplitStreamBench.php
-
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Patchlevel\\\\EventSourcing\\\\Tests\\\\Integration\\\\BankAccountSplitStream\\\\BankAccount'' and Patchlevel\\EventSourcing\\Tests\\Integration\\BankAccountSplitStream\\BankAccount will always evaluate to true\.$#'
identifier: staticMethod.alreadyNarrowedType
diff --git a/src/Aggregate/AggregateRoot.php b/src/Aggregate/AggregateRoot.php
index 84c6eabcf..b9c7c1203 100644
--- a/src/Aggregate/AggregateRoot.php
+++ b/src/Aggregate/AggregateRoot.php
@@ -4,9 +4,11 @@
namespace Patchlevel\EventSourcing\Aggregate;
+use Patchlevel\EventSourcing\Identifier\Identifier;
+
interface AggregateRoot
{
- public function aggregateRootId(): AggregateRootId;
+ public function aggregateRootId(): Identifier;
/** @param iterable