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 $events */ public function catchUp(iterable $events): void; diff --git a/src/Aggregate/AggregateRootAttributeBehaviour.php b/src/Aggregate/AggregateRootAttributeBehaviour.php index e8756081f..69ee4bf7b 100644 --- a/src/Aggregate/AggregateRootAttributeBehaviour.php +++ b/src/Aggregate/AggregateRootAttributeBehaviour.php @@ -4,6 +4,9 @@ namespace Patchlevel\EventSourcing\Aggregate; +use Patchlevel\Hydrator\Attribute\Ignore; +use ReflectionProperty; + use function array_key_exists; trait AggregateRootAttributeBehaviour @@ -11,6 +14,9 @@ trait AggregateRootAttributeBehaviour use AggregateRootBehaviour; use AggregateRootMetadataAwareBehaviour; + #[Ignore] + private AggregateRootId|null $cachedAggregateRootId = null; + protected function apply(object $event): void { $metadata = static::metadata(); @@ -26,4 +32,24 @@ protected function apply(object $event): void $method = $metadata->applyMethods[$event::class]; $this->$method($event); } + + public function aggregateRootId(): AggregateRootId + { + if ($this->cachedAggregateRootId instanceof AggregateRootId) { + return $this->cachedAggregateRootId; + } + + $metadata = static::metadata(); + + $reflection = new ReflectionProperty($this, $metadata->idProperty); + + /** @var mixed $aggregateRootId */ + $aggregateRootId = $reflection->getValue($this); + + if (!$aggregateRootId instanceof AggregateRootId) { + throw new AggregateRootIdNotSupported($this::class, $aggregateRootId); + } + + return $this->cachedAggregateRootId = $aggregateRootId; + } } diff --git a/src/Aggregate/AggregateRootId.php b/src/Aggregate/AggregateRootId.php new file mode 100644 index 000000000..5bbb66491 --- /dev/null +++ b/src/Aggregate/AggregateRootId.php @@ -0,0 +1,12 @@ + $aggregate + * @param class-string $aggregateRootClass * @param class-string $event */ - public function __construct(string $aggregate, string $event) + public function __construct(string $aggregateRootClass, string $event) { parent::__construct( sprintf( 'Apply method in "%s" could not be found for the event "%s"', - $aggregate, + $aggregateRootClass, $event, ), ); diff --git a/src/Aggregate/CustomId.php b/src/Aggregate/CustomId.php new file mode 100644 index 000000000..7335e2581 --- /dev/null +++ b/src/Aggregate/CustomId.php @@ -0,0 +1,10 @@ +id; + } +} diff --git a/src/Aggregate/RamseyUuidBehaviour.php b/src/Aggregate/RamseyUuidBehaviour.php new file mode 100644 index 000000000..a87301c00 --- /dev/null +++ b/src/Aggregate/RamseyUuidBehaviour.php @@ -0,0 +1,36 @@ +id->toString(); + } + + public static function v6(): self + { + return new self(Uuid::uuid6()); + } + + public static function v7(): self + { + return new self(Uuid::uuid7()); + } +} diff --git a/src/Aggregate/Uuid.php b/src/Aggregate/Uuid.php new file mode 100644 index 000000000..41144d4ac --- /dev/null +++ b/src/Aggregate/Uuid.php @@ -0,0 +1,10 @@ + */ public readonly string $className, public readonly string $name, + public readonly string $idProperty, /** @var array */ public readonly array $applyMethods, /** @var array */ diff --git a/src/Metadata/AggregateRoot/AggregateRootNameNotRegistered.php b/src/Metadata/AggregateRoot/AggregateRootNameNotRegistered.php index db7f5b145..ce6519da1 100644 --- a/src/Metadata/AggregateRoot/AggregateRootNameNotRegistered.php +++ b/src/Metadata/AggregateRoot/AggregateRootNameNotRegistered.php @@ -10,12 +10,12 @@ final class AggregateRootNameNotRegistered extends MetadataException { - public function __construct(string $evenName) + public function __construct(string $name) { parent::__construct( sprintf( 'Aggregate root name "%s" is not registered', - $evenName, + $name, ), ); } diff --git a/src/Metadata/AggregateRoot/AggregateWithoutMetadataAware.php b/src/Metadata/AggregateRoot/AggregateWithoutMetadataAware.php index 9cd0f1577..0fdf90263 100644 --- a/src/Metadata/AggregateRoot/AggregateWithoutMetadataAware.php +++ b/src/Metadata/AggregateRoot/AggregateWithoutMetadataAware.php @@ -10,9 +10,9 @@ final class AggregateWithoutMetadataAware extends MetadataException { - /** @param class-string $class */ - public function __construct(string $class) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass) { - parent::__construct(sprintf('The class "%s" does not implements AggregateRootMetadataAware', $class)); + parent::__construct(sprintf('The class "%s" does not implements AggregateRootMetadataAware', $aggregateRootClass)); } } diff --git a/src/Metadata/AggregateRoot/AttributeAggregateRootMetadataFactory.php b/src/Metadata/AggregateRoot/AttributeAggregateRootMetadataFactory.php index 74b8cd39b..99a03aaa6 100644 --- a/src/Metadata/AggregateRoot/AttributeAggregateRootMetadataFactory.php +++ b/src/Metadata/AggregateRoot/AttributeAggregateRootMetadataFactory.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\AggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Snapshot as AttributeSnapshot; use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; use ReflectionClass; @@ -41,6 +42,7 @@ public function metadata(string $aggregate): AggregateRootMetadata $reflector = new ReflectionClass($aggregate); $aggregateName = $this->findAggregateName($reflector); + $idProperty = $this->findIdProperty($reflector); [$suppressEvents, $suppressAll] = $this->findSuppressMissingApply($reflector); $applyMethods = $this->findApplyMethods($reflector, $aggregate); $snapshot = $this->findSnapshot($reflector); @@ -48,6 +50,7 @@ public function metadata(string $aggregate): AggregateRootMetadata $metadata = new AggregateRootMetadata( $aggregate, $aggregateName, + $idProperty, $applyMethods, $suppressEvents, $suppressAll, @@ -97,6 +100,23 @@ private function findAggregateName(ReflectionClass $reflector): string return $aggregateAttribute->name(); } + private function findIdProperty(ReflectionClass $reflector): string + { + $properties = $reflector->getProperties(); + + foreach ($properties as $property) { + $attributes = $property->getAttributes(Id::class); + + if ($attributes === []) { + continue; + } + + return $property->getName(); + } + + throw new AggregateRootIdNotFound($reflector->getName()); + } + private function findSnapshot(ReflectionClass $reflector): Snapshot|null { $attributeReflectionList = $reflector->getAttributes(AttributeSnapshot::class); diff --git a/src/Metadata/AggregateRoot/DuplicateApplyMethod.php b/src/Metadata/AggregateRoot/DuplicateApplyMethod.php index 2e648ea0a..8e5107317 100644 --- a/src/Metadata/AggregateRoot/DuplicateApplyMethod.php +++ b/src/Metadata/AggregateRoot/DuplicateApplyMethod.php @@ -12,17 +12,17 @@ final class DuplicateApplyMethod extends MetadataException { /** - * @param class-string $aggregate + * @param class-string $aggregateRootClass * @param class-string $event */ - public function __construct(string $aggregate, string $event, string $fistMethod, string $secondMethod) + public function __construct(string $aggregateRootClass, string $event, string $fistMethod, string $secondMethod) { parent::__construct( sprintf( 'Two methods "%s" and "%s" on the aggregate "%s" want to apply the same event "%s". Only one method can apply an event.', $fistMethod, $secondMethod, - $aggregate, + $aggregateRootClass, $event, ), ); diff --git a/src/Metadata/AggregateRoot/NoAggregateRoot.php b/src/Metadata/AggregateRoot/NoAggregateRoot.php index ece78fb91..ba3f9ecbc 100644 --- a/src/Metadata/AggregateRoot/NoAggregateRoot.php +++ b/src/Metadata/AggregateRoot/NoAggregateRoot.php @@ -10,9 +10,9 @@ final class NoAggregateRoot extends MetadataException { - /** @param class-string $class */ - public function __construct(string $class) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass) { - parent::__construct(sprintf('The class "%s" does not implement AggregateRoot', $class)); + parent::__construct(sprintf('The class "%s" does not implement AggregateRoot', $aggregateRootClass)); } } diff --git a/src/Metadata/Event/EventNameNotRegistered.php b/src/Metadata/Event/EventNameNotRegistered.php index 663d4497c..bc113c167 100644 --- a/src/Metadata/Event/EventNameNotRegistered.php +++ b/src/Metadata/Event/EventNameNotRegistered.php @@ -10,12 +10,12 @@ final class EventNameNotRegistered extends MetadataException { - public function __construct(string $evenName) + public function __construct(string $name) { parent::__construct( sprintf( 'Event name "%s" is not registered', - $evenName, + $name, ), ); } diff --git a/src/Repository/AggregateDetached.php b/src/Repository/AggregateDetached.php index f213838fb..eb164cb9f 100644 --- a/src/Repository/AggregateDetached.php +++ b/src/Repository/AggregateDetached.php @@ -5,19 +5,20 @@ namespace Patchlevel\EventSourcing\Repository; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use function sprintf; final class AggregateDetached extends RepositoryException { - /** @param class-string $aggregateClass */ - public function __construct(string $aggregateClass, string $aggregateId) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass, AggregateRootId $aggregateRootId) { parent::__construct( sprintf( 'An error occurred while saving the aggregate "%s" with the ID "%s", causing the uncommitted events to be lost. Please reload the aggregate.', - $aggregateClass, - $aggregateId, + $aggregateRootClass, + $aggregateRootId->toString(), ), ); } diff --git a/src/Repository/AggregateNotFound.php b/src/Repository/AggregateNotFound.php index 07d40b75b..33e864725 100644 --- a/src/Repository/AggregateNotFound.php +++ b/src/Repository/AggregateNotFound.php @@ -4,12 +4,14 @@ namespace Patchlevel\EventSourcing\Repository; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + use function sprintf; final class AggregateNotFound extends RepositoryException { - public function __construct(string $aggregateClass, string $id) + public function __construct(string $aggregateRootClass, AggregateRootId $rootId) { - parent::__construct(sprintf('aggregate "%s::%s" not found', $aggregateClass, $id)); + parent::__construct(sprintf('aggregate "%s::%s" not found', $aggregateRootClass, $rootId->toString())); } } diff --git a/src/Repository/AggregateUnknown.php b/src/Repository/AggregateUnknown.php index d7889811c..bbdad3f45 100644 --- a/src/Repository/AggregateUnknown.php +++ b/src/Repository/AggregateUnknown.php @@ -5,19 +5,20 @@ namespace Patchlevel\EventSourcing\Repository; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use function sprintf; final class AggregateUnknown extends RepositoryException { - /** @param class-string $aggregateClass */ - public function __construct(string $aggregateClass, string $aggregateId) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass, AggregateRootId $aggregateRootId) { parent::__construct( sprintf( 'The aggregate %s with the ID "%s" was not loaded from this repository. Please reload the aggregate.', - $aggregateClass, - $aggregateId, + $aggregateRootClass, + $aggregateRootId->toString(), ), ); } diff --git a/src/Repository/DefaultRepository.php b/src/Repository/DefaultRepository.php index c313a71e8..c6eb7fe28 100644 --- a/src/Repository/DefaultRepository.php +++ b/src/Repository/DefaultRepository.php @@ -5,6 +5,7 @@ namespace Patchlevel\EventSourcing\Repository; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Clock\SystemClock; use Patchlevel\EventSourcing\EventBus\Decorator\MessageDecorator; use Patchlevel\EventSourcing\EventBus\EventBus; @@ -57,7 +58,7 @@ public function __construct( } /** @return T */ - public function load(string $id): AggregateRoot + public function load(AggregateRootId $id): AggregateRoot { if ($this->snapshotStore && $this->metadata->snapshot) { try { @@ -69,7 +70,7 @@ public function load(string $id): AggregateRoot sprintf( 'snapshot for aggregate "%s" with the id "%s" not found', $this->metadata->className, - $id, + $id->toString(), ), ); } catch (SnapshotVersionInvalid) { @@ -77,7 +78,7 @@ public function load(string $id): AggregateRoot sprintf( 'snapshot for aggregate "%s" with the id "%s" is invalid', $this->metadata->className, - $id, + $id->toString(), ), ); } @@ -85,7 +86,7 @@ public function load(string $id): AggregateRoot $criteria = (new CriteriaBuilder()) ->aggregateClass($this->metadata->className) - ->aggregateId($id) + ->aggregateId($id->toString()) ->archived(false) ->build(); @@ -117,11 +118,11 @@ public function load(string $id): AggregateRoot return $aggregate; } - public function has(string $id): bool + public function has(AggregateRootId $id): bool { $criteria = (new CriteriaBuilder()) ->aggregateClass($this->metadata->className) - ->aggregateId($id) + ->aggregateId($id->toString()) ->build(); return $this->store->count($criteria) > 0; @@ -162,7 +163,7 @@ public function save(AggregateRoot $aggregate): void static function (object $event) use ($aggregate, &$playhead, $messageDecorator, $clock) { $message = Message::create($event) ->withAggregateClass($aggregate::class) - ->withAggregateId($aggregate->aggregateRootId()) + ->withAggregateId($aggregate->aggregateRootId()->toString()) ->withPlayhead(++$playhead) ->withRecordedOn($clock->now()); @@ -194,7 +195,7 @@ static function (object $event) use ($aggregate, &$playhead, $messageDecorator, * * @return T */ - private function loadFromSnapshot(string $aggregateClass, string $id): AggregateRoot + private function loadFromSnapshot(string $aggregateClass, AggregateRootId $id): AggregateRoot { assert($this->snapshotStore instanceof SnapshotStore); @@ -202,7 +203,7 @@ private function loadFromSnapshot(string $aggregateClass, string $id): Aggregate $criteria = (new CriteriaBuilder()) ->aggregateClass($this->metadata->className) - ->aggregateId($id) + ->aggregateId($id->toString()) ->fromPlayhead($aggregate->playhead()) ->build(); diff --git a/src/Repository/PlayheadMismatch.php b/src/Repository/PlayheadMismatch.php index 54eaaecff..d8199b9a8 100644 --- a/src/Repository/PlayheadMismatch.php +++ b/src/Repository/PlayheadMismatch.php @@ -4,18 +4,20 @@ namespace Patchlevel\EventSourcing\Repository; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + use function sprintf; final class PlayheadMismatch extends RepositoryException { - public function __construct(string $aggregateClass, string $aggregateId, int $playhead, int $eventCount) + public function __construct(string $aggregateRootClass, AggregateRootId $aggregateRootId, int $playhead, int $eventCount) { parent::__construct(sprintf( 'There is a mismatch between the playhead [%s] and the event count [%s] for the aggregate [%s] with the id [%s]', $playhead, $eventCount, - $aggregateClass, - $aggregateId, + $aggregateRootClass, + $aggregateRootId->toString(), )); } } diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php index 253732aa3..1ad8f36c4 100644 --- a/src/Repository/Repository.php +++ b/src/Repository/Repository.php @@ -5,14 +5,15 @@ namespace Patchlevel\EventSourcing\Repository; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; /** @template T of AggregateRoot */ interface Repository { /** @return T */ - public function load(string $id): AggregateRoot; + public function load(AggregateRootId $id): AggregateRoot; - public function has(string $id): bool; + public function has(AggregateRootId $id): bool; /** @param T $aggregate */ public function save(AggregateRoot $aggregate): void; diff --git a/src/Repository/SnapshotRebuildFailed.php b/src/Repository/SnapshotRebuildFailed.php index 9c46ab2ac..a4d5aa74b 100644 --- a/src/Repository/SnapshotRebuildFailed.php +++ b/src/Repository/SnapshotRebuildFailed.php @@ -5,23 +5,24 @@ namespace Patchlevel\EventSourcing\Repository; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Throwable; use function sprintf; final class SnapshotRebuildFailed extends RepositoryException { - /** @param class-string $aggregateClass */ + /** @param class-string $aggregateRootClass */ public function __construct( - private string $aggregateClass, - private string $aggregateId, + private string $aggregateRootClass, + private AggregateRootId $aggregateRootId, Throwable $previous, ) { parent::__construct( sprintf( 'Rebuild from snapshot of aggregate "%s" with the id "%s" failed', - $aggregateClass, - $aggregateId, + $aggregateRootClass, + $aggregateRootId->toString(), ), 0, $previous, @@ -31,11 +32,11 @@ public function __construct( /** @return class-string */ public function aggregateClass(): string { - return $this->aggregateClass; + return $this->aggregateRootClass; } - public function aggregateId(): string + public function aggregateRootId(): AggregateRootId { - return $this->aggregateId; + return $this->aggregateRootId; } } diff --git a/src/Repository/WrongAggregate.php b/src/Repository/WrongAggregate.php index 3c864a671..3536e877b 100644 --- a/src/Repository/WrongAggregate.php +++ b/src/Repository/WrongAggregate.php @@ -9,13 +9,13 @@ final class WrongAggregate extends RepositoryException { /** - * @param class-string $aggregateClass + * @param class-string $aggregateRootClass * @param class-string $expected */ - public function __construct(string $aggregateClass, string $expected) + public function __construct(string $aggregateRootClass, string $expected) { parent::__construct( - sprintf('Wrong aggregate given: got "%s" but expected "%s"', $aggregateClass, $expected), + sprintf('Wrong aggregate given: got "%s" but expected "%s"', $aggregateRootClass, $expected), ); } } diff --git a/src/Serializer/Normalizer/IdNormalizer.php b/src/Serializer/Normalizer/IdNormalizer.php new file mode 100644 index 000000000..9c911c33d --- /dev/null +++ b/src/Serializer/Normalizer/IdNormalizer.php @@ -0,0 +1,50 @@ + */ + private readonly string $aggregateIdClass, + ) { + } + + public function normalize(mixed $value): string|null + { + if ($value === null) { + return null; + } + + if (!$value instanceof AggregateRootId) { + throw InvalidArgument::withWrongType($this->aggregateIdClass, $value); + } + + return $value->toString(); + } + + public function denormalize(mixed $value): AggregateRootId|null + { + if ($value === null) { + return null; + } + + if (!is_string($value)) { + throw InvalidArgument::withWrongType('string', $value); + } + + $class = $this->aggregateIdClass; + + return $class::fromString($value); + } +} diff --git a/src/Snapshot/DefaultSnapshotStore.php b/src/Snapshot/DefaultSnapshotStore.php index eddafee69..08ddbaaa6 100644 --- a/src/Snapshot/DefaultSnapshotStore.php +++ b/src/Snapshot/DefaultSnapshotStore.php @@ -5,6 +5,7 @@ namespace Patchlevel\EventSourcing\Snapshot; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataAwareMetadataFactory; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataFactory; use Patchlevel\EventSourcing\Snapshot\Adapter\SnapshotAdapter; @@ -56,7 +57,7 @@ public function save(AggregateRoot $aggregateRoot): void * * @template T of AggregateRoot */ - public function load(string $aggregateClass, string $id): AggregateRoot + public function load(string $aggregateClass, AggregateRootId $id): AggregateRoot { $adapter = $this->adapter($aggregateClass); $key = $this->key($aggregateClass, $id); @@ -98,11 +99,11 @@ public function adapter(string $aggregateClass): SnapshotAdapter } /** @param class-string $aggregateClass */ - private function key(string $aggregateClass, string $aggregateId): string + private function key(string $aggregateClass, AggregateRootId $aggregateId): string { $aggregateName = $this->metadataFactory->metadata($aggregateClass)->name; - return sprintf('%s-%s', $aggregateName, $aggregateId); + return sprintf('%s-%s', $aggregateName, $aggregateId->toString()); } /** @param class-string $aggregateClass */ diff --git a/src/Snapshot/SnapshotNotConfigured.php b/src/Snapshot/SnapshotNotConfigured.php index bd948a13a..ef862d512 100644 --- a/src/Snapshot/SnapshotNotConfigured.php +++ b/src/Snapshot/SnapshotNotConfigured.php @@ -10,13 +10,13 @@ final class SnapshotNotConfigured extends SnapshotException { - /** @param class-string $aggregateClass */ - public function __construct(string $aggregateClass) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass) { parent::__construct( sprintf( 'Missing snapshot configuration for the aggregate class "%s"', - $aggregateClass, + $aggregateRootClass, ), ); } diff --git a/src/Snapshot/SnapshotNotFound.php b/src/Snapshot/SnapshotNotFound.php index 455de4924..e41f69234 100644 --- a/src/Snapshot/SnapshotNotFound.php +++ b/src/Snapshot/SnapshotNotFound.php @@ -5,20 +5,21 @@ namespace Patchlevel\EventSourcing\Snapshot; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Throwable; use function sprintf; final class SnapshotNotFound extends SnapshotException { - /** @param class-string $aggregate */ - public function __construct(string $aggregate, string $id, Throwable|null $previous = null) + /** @param class-string $aggregateRootClass */ + public function __construct(string $aggregateRootClass, AggregateRootId $rootId, Throwable|null $previous = null) { parent::__construct( sprintf( 'snapshot for aggregate "%s" with the id "%s" not found', - $aggregate, - $id, + $aggregateRootClass, + $rootId->toString(), ), 0, $previous, diff --git a/src/Snapshot/SnapshotStore.php b/src/Snapshot/SnapshotStore.php index d0bc2ee74..d6d14d339 100644 --- a/src/Snapshot/SnapshotStore.php +++ b/src/Snapshot/SnapshotStore.php @@ -5,6 +5,7 @@ namespace Patchlevel\EventSourcing\Snapshot; use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\Snapshot\Adapter\SnapshotNotFound; interface SnapshotStore @@ -20,5 +21,5 @@ public function save(AggregateRoot $aggregateRoot): void; * * @template T of AggregateRoot */ - public function load(string $aggregateClass, string $id): AggregateRoot; + public function load(string $aggregateClass, AggregateRootId $id): AggregateRoot; } diff --git a/tests/Benchmark/BasicImplementation/Aggregate/Profile.php b/tests/Benchmark/BasicImplementation/Aggregate/Profile.php index 9d02bcc8c..5f994fddb 100644 --- a/tests/Benchmark/BasicImplementation/Aggregate/Profile.php +++ b/tests/Benchmark/BasicImplementation/Aggregate/Profile.php @@ -7,26 +7,23 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Snapshot; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\NameChanged; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events\Reborn; -use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Normalizer\ProfileIdNormalizer; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId; #[Aggregate('profile')] #[Snapshot('default')] final class Profile extends BasicAggregateRoot { - #[ProfileIdNormalizer] + #[Id] + #[IdNormalizer(ProfileId::class)] private ProfileId $id; private string $name; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id, string $name): self { $self = new self(); diff --git a/tests/Benchmark/BasicImplementation/Events/ProfileCreated.php b/tests/Benchmark/BasicImplementation/Events/ProfileCreated.php index c2be67f57..c83359156 100644 --- a/tests/Benchmark/BasicImplementation/Events/ProfileCreated.php +++ b/tests/Benchmark/BasicImplementation/Events/ProfileCreated.php @@ -5,14 +5,14 @@ namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Events; use Patchlevel\EventSourcing\Attribute\Event; -use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Normalizer\ProfileIdNormalizer; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId; #[Event('profile.created')] final class ProfileCreated { public function __construct( - #[ProfileIdNormalizer] + #[IdNormalizer(ProfileId::class)] public ProfileId $profileId, public string $name, ) { diff --git a/tests/Benchmark/BasicImplementation/Events/Reborn.php b/tests/Benchmark/BasicImplementation/Events/Reborn.php index e4d7e9ef2..16f9dc9d6 100644 --- a/tests/Benchmark/BasicImplementation/Events/Reborn.php +++ b/tests/Benchmark/BasicImplementation/Events/Reborn.php @@ -6,7 +6,7 @@ use Patchlevel\EventSourcing\Attribute\Event; use Patchlevel\EventSourcing\Attribute\SplitStream; -use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\Normalizer\ProfileIdNormalizer; +use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer; use Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation\ProfileId; #[Event('profile.reborn')] @@ -14,7 +14,7 @@ final class Reborn { public function __construct( - #[ProfileIdNormalizer] + #[IdNormalizer(ProfileId::class)] public ProfileId $profileId, public string $name, ) { diff --git a/tests/Benchmark/BasicImplementation/Normalizer/ProfileIdNormalizer.php b/tests/Benchmark/BasicImplementation/Normalizer/ProfileIdNormalizer.php deleted file mode 100644 index 84b54e082..000000000 --- a/tests/Benchmark/BasicImplementation/Normalizer/ProfileIdNormalizer.php +++ /dev/null @@ -1,38 +0,0 @@ -toString(); - } - - public function denormalize(mixed $value): ProfileId|null - { - if ($value === null) { - return null; - } - - if (!is_string($value)) { - throw new InvalidArgumentException(); - } - - return ProfileId::fromString($value); - } -} diff --git a/tests/Benchmark/BasicImplementation/ProfileId.php b/tests/Benchmark/BasicImplementation/ProfileId.php index 188a496b4..a05a9cd58 100644 --- a/tests/Benchmark/BasicImplementation/ProfileId.php +++ b/tests/Benchmark/BasicImplementation/ProfileId.php @@ -4,27 +4,10 @@ namespace Patchlevel\EventSourcing\Tests\Benchmark\BasicImplementation; -use function uniqid; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; +use Patchlevel\EventSourcing\Aggregate\RamseyUuidBehaviour; -final class ProfileId +final class ProfileId implements AggregateRootId { - private function __construct( - private string $id, - ) { - } - - public static function fromString(string $id): self - { - return new self($id); - } - - public function toString(): string - { - return $this->id; - } - - public static function generate(): self - { - return new self(uniqid('', true)); - } + use RamseyUuidBehaviour; } diff --git a/tests/Benchmark/SimpleSetupBench.php b/tests/Benchmark/SimpleSetupBench.php index 914a8183b..a3db07219 100644 --- a/tests/Benchmark/SimpleSetupBench.php +++ b/tests/Benchmark/SimpleSetupBench.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\PDO\SQLite\Driver; use Doctrine\DBAL\DriverManager; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\EventBus\EventBus; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; @@ -31,6 +32,8 @@ final class SimpleSetupBench private EventBus $bus; private Repository $repository; + private AggregateRootId $id; + public function setUp(): void { if (file_exists(self::DB_PATH)) { @@ -60,7 +63,9 @@ public function setUp(): void $schemaDirector->create(); - $profile = Profile::create(ProfileId::fromString('1'), 'Peter'); + $this->id = ProfileId::v7(); + + $profile = Profile::create($this->id, 'Peter'); for ($i = 0; $i < 10_000; $i++) { $profile->changeName('Peter'); @@ -72,20 +77,20 @@ public function setUp(): void #[Bench\Revs(20)] public function benchLoad10000Events(): void { - $this->repository->load('1'); + $this->repository->load($this->id); } #[Bench\Revs(20)] public function benchSave1Event(): void { - $profile = Profile::create(ProfileId::generate(), 'Peter'); + $profile = Profile::create(ProfileId::v7(), 'Peter'); $this->repository->save($profile); } #[Bench\Revs(20)] public function benchSave10000Events(): void { - $profile = Profile::create(ProfileId::generate(), 'Peter'); + $profile = Profile::create(ProfileId::v7(), 'Peter'); for ($i = 1; $i < 10_000; $i++) { $profile->changeName('Peter'); @@ -98,7 +103,7 @@ public function benchSave10000Events(): void public function benchSave10000Aggregates(): void { for ($i = 1; $i < 10_000; $i++) { - $profile = Profile::create(ProfileId::generate(), 'Peter'); + $profile = Profile::create(ProfileId::v7(), 'Peter'); $this->repository->save($profile); } } @@ -108,7 +113,7 @@ public function benchSave10000AggregatesTransaction(): void { $this->store->transactional(function (): void { for ($i = 1; $i < 10_000; $i++) { - $profile = Profile::create(ProfileId::generate(), 'Peter'); + $profile = Profile::create(ProfileId::v7(), 'Peter'); $this->repository->save($profile); } }); diff --git a/tests/Benchmark/SnapshotsBench.php b/tests/Benchmark/SnapshotsBench.php index 0a14eee32..411989cec 100644 --- a/tests/Benchmark/SnapshotsBench.php +++ b/tests/Benchmark/SnapshotsBench.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\PDO\SQLite\Driver; use Doctrine\DBAL\DriverManager; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\EventBus\EventBus; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; @@ -37,6 +38,8 @@ final class SnapshotsBench private InMemorySnapshotAdapter $adapter; + private AggregateRootId $id; + public function setUp(): void { if (file_exists(self::DB_PATH)) { @@ -70,7 +73,8 @@ public function setUp(): void $schemaDirector->create(); - $profile = Profile::create(ProfileId::fromString('1'), 'Peter'); + $this->id = ProfileId::v7(); + $profile = Profile::create($this->id, 'Peter'); for ($i = 0; $i < 10_000; $i++) { $profile->changeName('Peter'); @@ -84,12 +88,12 @@ public function setUp(): void public function benchLoad10000EventsMissingSnapshot(): void { $this->adapter->clear(); - $this->repository->load('1'); + $this->repository->load($this->id); } #[Bench\Revs(20)] public function benchLoad10000Events(): void { - $this->repository->load('1'); + $this->repository->load($this->id); } } diff --git a/tests/Benchmark/SplitStreamBench.php b/tests/Benchmark/SplitStreamBench.php index 2ebaeb7ee..db3413f68 100644 --- a/tests/Benchmark/SplitStreamBench.php +++ b/tests/Benchmark/SplitStreamBench.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Driver\PDO\SQLite\Driver; use Doctrine\DBAL\DriverManager; +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; use Patchlevel\EventSourcing\EventBus\Decorator\SplitStreamDecorator; use Patchlevel\EventSourcing\EventBus\DefaultEventBus; use Patchlevel\EventSourcing\EventBus\EventBus; @@ -36,6 +37,8 @@ final class SplitStreamBench private SnapshotStore $snapshotStore; private Repository $repository; + private AggregateRootId $id; + public function setUp(): void { if (file_exists(self::DB_PATH)) { @@ -72,11 +75,13 @@ public function setUp(): void ); $schemaDirector->create(); + + $this->id = ProfileId::v7(); } public function provideData(): void { - $profile = Profile::create(ProfileId::fromString('1'), 'Peter'); + $profile = Profile::create($this->id, 'Peter'); for ($i = 0; $i < 10_000; $i++) { $profile->changeName(sprintf('Peter %d', $i)); @@ -95,13 +100,13 @@ public function provideData(): void #[Bench\BeforeMethods('provideData')] public function benchLoad10000Events(): void { - $this->repository->load('1'); + $this->repository->load($this->id); } #[Bench\Revs(20)] public function benchSave10000Events(): void { - $profile = Profile::create(ProfileId::generate(), 'Peter'); + $profile = Profile::create(ProfileId::v7(), 'Peter'); for ($i = 0; $i < 10_000; $i++) { $profile->changeName(sprintf('Peter %d', $i)); diff --git a/tests/Benchmark/SyncProjectionistBench.php b/tests/Benchmark/SyncProjectionistBench.php index 028eec29c..667f36b24 100644 --- a/tests/Benchmark/SyncProjectionistBench.php +++ b/tests/Benchmark/SyncProjectionistBench.php @@ -42,6 +42,8 @@ final class SyncProjectionistBench private Repository $repository; private Profile $profile; + private ProfileId $id; + public function setUp(): void { if (file_exists(self::DB_PATH)) { @@ -100,7 +102,8 @@ public function setUp(): void $schemaDirector->create(); $projectionist->boot(); - $this->profile = Profile::create(ProfileId::fromString('1'), 'Peter'); + $this->id = ProfileId::v7(); + $this->profile = Profile::create($this->id, 'Peter'); $this->repository->save($this->profile); } diff --git a/tests/Benchmark/blackfire.php b/tests/Benchmark/blackfire.php index db4216d9a..fea73a725 100644 --- a/tests/Benchmark/blackfire.php +++ b/tests/Benchmark/blackfire.php @@ -44,11 +44,15 @@ $schemaDirector->create(); -$id = ProfileId::generate(); -$profile = Profile::create($id, 'Peter'); - -for ($i = 0; $i < 10_000; $i++) { - $profile->changeName('Peter ' . $i); -} - -$repository->save($profile); +$store->transactional(static function () use ($repository): void { + for ($i = 0; $i < 10_000; $i++) { + $id = ProfileId::v7(); + $profile = Profile::create($id, 'Peter'); + + for ($j = 0; $j < 10; $j++) { + $profile->changeName('Peter ' . $j); + } + + $repository->save($profile); + } +}); diff --git a/tests/Integration/BankAccountSplitStream/AccountId.php b/tests/Integration/BankAccountSplitStream/AccountId.php index ba7c33eab..bd3bf2b10 100644 --- a/tests/Integration/BankAccountSplitStream/AccountId.php +++ b/tests/Integration/BankAccountSplitStream/AccountId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream; -final class AccountId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class AccountId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Integration/BankAccountSplitStream/Aggregate/BankAccount.php b/tests/Integration/BankAccountSplitStream/Aggregate/BankAccount.php index 609ddd0d3..61f4dd5b1 100644 --- a/tests/Integration/BankAccountSplitStream/Aggregate/BankAccount.php +++ b/tests/Integration/BankAccountSplitStream/Aggregate/BankAccount.php @@ -7,27 +7,22 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\AccountId; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\BalanceAdded; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\BankAccountCreated; use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Events\MonthPassed; -use Patchlevel\EventSourcing\Tests\Integration\BankAccountSplitStream\Normalizer\AccountIdNormalizer; #[Aggregate('profile')] final class BankAccount extends BasicAggregateRoot { - #[AccountIdNormalizer] + #[Id] private AccountId $id; private string $name; private int $balanceInCents; /** @var list */ public array $appliedEvents = []; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(AccountId $id, string $name): self { $self = new self(); diff --git a/tests/Integration/BankAccountSplitStream/IntegrationTest.php b/tests/Integration/BankAccountSplitStream/IntegrationTest.php index 734230f43..d22ec17b7 100644 --- a/tests/Integration/BankAccountSplitStream/IntegrationTest.php +++ b/tests/Integration/BankAccountSplitStream/IntegrationTest.php @@ -91,7 +91,8 @@ public function testSuccessful(): void $schemaDirector->create(); $projectionist->boot(); - $bankAccount = BankAccount::create(AccountId::fromString('1'), 'John'); + $bankAccountId = AccountId::fromString('1'); + $bankAccount = BankAccount::create($bankAccountId, 'John'); $bankAccount->addBalance(100); $bankAccount->addBalance(500); $repository->save($bankAccount); @@ -114,10 +115,10 @@ public function testSuccessful(): void ]), ); $repository = $manager->get(BankAccount::class); - $bankAccount = $repository->load('1'); + $bankAccount = $repository->load($bankAccountId); self::assertInstanceOf(BankAccount::class, $bankAccount); - self::assertSame('1', $bankAccount->aggregateRootId()); + self::assertEquals($bankAccountId, $bankAccount->aggregateRootId()); self::assertSame(3, $bankAccount->playhead()); self::assertSame('John', $bankAccount->name()); self::assertSame(600, $bankAccount->balance()); @@ -148,10 +149,10 @@ public function testSuccessful(): void ]), ); $repository = $manager->get(BankAccount::class); - $bankAccount = $repository->load('1'); + $bankAccount = $repository->load($bankAccountId); self::assertInstanceOf(BankAccount::class, $bankAccount); - self::assertSame('1', $bankAccount->aggregateRootId()); + self::assertEquals($bankAccountId, $bankAccount->aggregateRootId()); self::assertSame(5, $bankAccount->playhead()); self::assertSame('John', $bankAccount->name()); self::assertSame(800, $bankAccount->balance()); diff --git a/tests/Integration/BasicImplementation/Aggregate/Profile.php b/tests/Integration/BasicImplementation/Aggregate/Profile.php index bd77fdc79..34b90b38c 100644 --- a/tests/Integration/BasicImplementation/Aggregate/Profile.php +++ b/tests/Integration/BasicImplementation/Aggregate/Profile.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Snapshot; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Normalizer\ProfileIdNormalizer; @@ -16,15 +17,11 @@ #[Snapshot('default', 100)] final class Profile extends BasicAggregateRoot { + #[Id] #[ProfileIdNormalizer] private ProfileId $id; private string $name; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id, string $name): self { $self = new self(); diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index 61c8f8e8d..d55cd17ce 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -92,7 +92,8 @@ public function testSuccessful(): void $schemaDirector->create(); $projectionist->boot(); - $profile = Profile::create(ProfileId::fromString('1'), 'John'); + $profileId = ProfileId::fromString('1'); + $profile = Profile::create($profileId, 'John'); $repository->save($profile); $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile WHERE id = ?', ['1']); @@ -108,10 +109,10 @@ public function testSuccessful(): void $eventStream, ); $repository = $manager->get(Profile::class); - $profile = $repository->load('1'); + $profile = $repository->load($profileId); self::assertInstanceOf(Profile::class, $profile); - self::assertSame('1', $profile->aggregateRootId()); + self::assertEquals($profileId, $profile->aggregateRootId()); self::assertSame(1, $profile->playhead()); self::assertSame('John', $profile->name()); self::assertSame(1, SendEmailMock::count()); @@ -165,7 +166,8 @@ public function testWithSymfonySuccessful(): void $schemaDirector->create(); $projectionist->boot(); - $profile = Profile::create(ProfileId::fromString('1'), 'John'); + $profileId = ProfileId::fromString('1'); + $profile = Profile::create($profileId, 'John'); $repository->save($profile); $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile WHERE id = ?', ['1']); @@ -184,10 +186,10 @@ public function testWithSymfonySuccessful(): void ); $repository = $manager->get(Profile::class); - $profile = $repository->load('1'); + $profile = $repository->load($profileId); self::assertInstanceOf(Profile::class, $profile); - self::assertSame('1', $profile->aggregateRootId()); + self::assertEquals($profileId, $profile->aggregateRootId()); self::assertSame(1, $profile->playhead()); self::assertSame('John', $profile->name()); self::assertSame(1, SendEmailMock::count()); @@ -241,7 +243,8 @@ public function testSnapshot(): void $schemaDirector->create(); $projectionist->boot(); - $profile = Profile::create(ProfileId::fromString('1'), 'John'); + $profileId = ProfileId::fromString('1'); + $profile = Profile::create($profileId, 'John'); $repository->save($profile); $result = $this->connection->fetchAssociative('SELECT * FROM projection_profile WHERE id = ?', ['1']); @@ -257,10 +260,10 @@ public function testSnapshot(): void $eventStream, ); $repository = $manager->get(Profile::class); - $profile = $repository->load('1'); + $profile = $repository->load($profileId); self::assertInstanceOf(Profile::class, $profile); - self::assertSame('1', $profile->aggregateRootId()); + self::assertEquals($profileId, $profile->aggregateRootId()); self::assertSame(1, $profile->playhead()); self::assertSame('John', $profile->name()); self::assertSame(1, SendEmailMock::count()); diff --git a/tests/Integration/BasicImplementation/ProfileId.php b/tests/Integration/BasicImplementation/ProfileId.php index f36b33c81..939153ea5 100644 --- a/tests/Integration/BasicImplementation/ProfileId.php +++ b/tests/Integration/BasicImplementation/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Integration/Outbox/Aggregate/Profile.php b/tests/Integration/Outbox/Aggregate/Profile.php index b4ceda4fd..8ed0ab53c 100644 --- a/tests/Integration/Outbox/Aggregate/Profile.php +++ b/tests/Integration/Outbox/Aggregate/Profile.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Snapshot; use Patchlevel\EventSourcing\Tests\Integration\Outbox\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\Outbox\Normalizer\ProfileIdNormalizer; @@ -16,15 +17,11 @@ #[Snapshot('default', 100)] final class Profile extends BasicAggregateRoot { + #[Id] #[ProfileIdNormalizer] private ProfileId $id; private string $name; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id, string $name): self { $self = new self(); diff --git a/tests/Integration/Outbox/ProfileId.php b/tests/Integration/Outbox/ProfileId.php index 68782a3aa..c94cb8a40 100644 --- a/tests/Integration/Outbox/ProfileId.php +++ b/tests/Integration/Outbox/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Outbox; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Integration/Pipeline/Aggregate/Profile.php b/tests/Integration/Pipeline/Aggregate/Profile.php index c7065ce5d..7bade7fb2 100644 --- a/tests/Integration/Pipeline/Aggregate/Profile.php +++ b/tests/Integration/Pipeline/Aggregate/Profile.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Tests\Integration\Pipeline\Events\NewVisited; use Patchlevel\EventSourcing\Tests\Integration\Pipeline\Events\OldVisited; use Patchlevel\EventSourcing\Tests\Integration\Pipeline\Events\PrivacyAdded; @@ -16,15 +17,11 @@ #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { + #[Id] private ProfileId $id; private bool $privacy; private int $visited; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id): self { $self = new self(); diff --git a/tests/Integration/Pipeline/PipelineChangeStoreTest.php b/tests/Integration/Pipeline/PipelineChangeStoreTest.php index 2d439acea..df3b0b993 100644 --- a/tests/Integration/Pipeline/PipelineChangeStoreTest.php +++ b/tests/Integration/Pipeline/PipelineChangeStoreTest.php @@ -78,7 +78,8 @@ public function testSuccessful(): void $oldRepository = new DefaultRepository($oldStore, new DefaultEventBus(), Profile::metadata()); $newRepository = new DefaultRepository($newStore, new DefaultEventBus(), Profile::metadata()); - $profile = Profile::create(ProfileId::fromString('1')); + $profileId = ProfileId::fromString('1'); + $profile = Profile::create($profileId); $profile->visit(); $profile->privacy(); $profile->visit(); @@ -86,7 +87,7 @@ public function testSuccessful(): void $oldRepository->save($profile); self::assertSame(4, $oldStore->count()); - self::assertSame('1', $profile->aggregateRootId()); + self::assertEquals($profileId, $profile->aggregateRootId()); self::assertSame(4, $profile->playhead()); self::assertSame(true, $profile->isPrivate()); self::assertSame(2, $profile->count()); @@ -106,10 +107,10 @@ public function testSuccessful(): void self::assertSame(4, $pipeline->count()); $pipeline->run(); - $newProfile = $newRepository->load('1'); + $newProfile = $newRepository->load($profileId); self::assertInstanceOf(Profile::class, $newProfile); - self::assertSame('1', $newProfile->aggregateRootId()); + self::assertEquals($profileId, $newProfile->aggregateRootId()); self::assertSame(3, $newProfile->playhead()); self::assertSame(false, $newProfile->isPrivate()); self::assertSame(-2, $newProfile->count()); diff --git a/tests/Integration/Pipeline/ProfileId.php b/tests/Integration/Pipeline/ProfileId.php index 87768e35a..236d4b3a2 100644 --- a/tests/Integration/Pipeline/ProfileId.php +++ b/tests/Integration/Pipeline/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Pipeline; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Integration/Projectionist/Aggregate/Profile.php b/tests/Integration/Projectionist/Aggregate/Profile.php index 8f1f7c39f..95cc98b20 100644 --- a/tests/Integration/Projectionist/Aggregate/Profile.php +++ b/tests/Integration/Projectionist/Aggregate/Profile.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Normalizer\ProfileIdNormalizer; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId; @@ -14,15 +15,11 @@ #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { + #[Id] #[ProfileIdNormalizer] private ProfileId $id; private string $name; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id, string $name): self { $self = new self(); diff --git a/tests/Integration/Projectionist/ProfileId.php b/tests/Integration/Projectionist/ProfileId.php index 085503b19..9d4992435 100644 --- a/tests/Integration/Projectionist/ProfileId.php +++ b/tests/Integration/Projectionist/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Projectionist; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Integration/Store/Profile.php b/tests/Integration/Store/Profile.php index 76bdea122..f78412d06 100644 --- a/tests/Integration/Store/Profile.php +++ b/tests/Integration/Store/Profile.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Events\ProfileCreated; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\Normalizer\ProfileIdNormalizer; use Patchlevel\EventSourcing\Tests\Integration\Projectionist\ProfileId; @@ -14,15 +15,11 @@ #[Aggregate('profile')] final class Profile extends BasicAggregateRoot { + #[Id] #[ProfileIdNormalizer] private ProfileId $id; private string $name; - public function aggregateRootId(): string - { - return $this->id->toString(); - } - public static function create(ProfileId $id, string $name): self { $self = new self(); diff --git a/tests/Integration/Store/ProfileId.php b/tests/Integration/Store/ProfileId.php index f434ccafe..3b7b3fcdd 100644 --- a/tests/Integration/Store/ProfileId.php +++ b/tests/Integration/Store/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Integration\Store; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct(private string $id) { diff --git a/tests/Unit/Aggregate/AggregateRootTest.php b/tests/Unit/Aggregate/AggregateRootTest.php index 612477b33..f8c9f23b8 100644 --- a/tests/Unit/Aggregate/AggregateRootTest.php +++ b/tests/Unit/Aggregate/AggregateRootTest.php @@ -29,7 +29,7 @@ public function testApplyMethod(): void $profile = Profile::createProfile($id, $email); - self::assertSame('1', $profile->aggregateRootId()); + self::assertSame($id, $profile->aggregateRootId()); self::assertSame(1, $profile->playhead()); self::assertEquals($id, $profile->id()); self::assertEquals($email, $profile->email()); @@ -53,7 +53,7 @@ public function testCreateFromMessages(): void $profile = Profile::createFromEvents([new ProfileCreated($id, $email)]); - self::assertSame('1', $profile->aggregateRootId()); + self::assertSame($id, $profile->aggregateRootId()); self::assertSame(1, $profile->playhead()); self::assertEquals($id, $profile->id()); self::assertEquals($email, $profile->email()); @@ -74,7 +74,7 @@ public function testMultipleApplyOnOneMethod(): void $profile = Profile::createProfile($id, $email); $profile->visitProfile($target); - self::assertSame('1', $profile->aggregateRootId()); + self::assertSame($id, $profile->aggregateRootId()); self::assertSame(2, $profile->playhead()); self::assertEquals($id, $profile->id()); self::assertEquals($email, $profile->email()); diff --git a/tests/Unit/Fixture/Profile.php b/tests/Unit/Fixture/Profile.php index 40e5a8335..3e1257b1d 100644 --- a/tests/Unit/Fixture/Profile.php +++ b/tests/Unit/Fixture/Profile.php @@ -7,12 +7,14 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; #[Aggregate('profile')] #[SuppressMissingApply([MessageDeleted::class])] final class Profile extends BasicAggregateRoot { + #[Id] private ProfileId $id; private Email $email; private int $visits = 0; @@ -89,9 +91,4 @@ protected function applySplittingEvent(SplittingEvent $event): void $this->email = $event->email; $this->visits = $event->visits; } - - public function aggregateRootId(): string - { - return $this->id->toString(); - } } diff --git a/tests/Unit/Fixture/ProfileId.php b/tests/Unit/Fixture/ProfileId.php index 566e80f37..55fb67218 100644 --- a/tests/Unit/Fixture/ProfileId.php +++ b/tests/Unit/Fixture/ProfileId.php @@ -4,7 +4,9 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Fixture; -final class ProfileId +use Patchlevel\EventSourcing\Aggregate\AggregateRootId; + +final class ProfileId implements AggregateRootId { private function __construct( private string $id, diff --git a/tests/Unit/Fixture/ProfileInvalid.php b/tests/Unit/Fixture/ProfileInvalid.php index b9e346240..7f31579c3 100644 --- a/tests/Unit/Fixture/ProfileInvalid.php +++ b/tests/Unit/Fixture/ProfileInvalid.php @@ -7,10 +7,12 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileInvalid::class)] final class ProfileInvalid extends BasicAggregateRoot { + #[Id] private ProfileId $id; private Email $email; @@ -35,9 +37,4 @@ protected function applyProfileCreated2(ProfileCreated $event): void $this->id = $event->profileId; $this->email = $event->email; } - - public function aggregateRootId(): string - { - return $this->id->toString(); - } } diff --git a/tests/Unit/Fixture/ProfileWithBrokenApplyBothUsage.php b/tests/Unit/Fixture/ProfileWithBrokenApplyBothUsage.php index 7f323882d..5e4c26919 100644 --- a/tests/Unit/Fixture/ProfileWithBrokenApplyBothUsage.php +++ b/tests/Unit/Fixture/ProfileWithBrokenApplyBothUsage.php @@ -7,18 +7,17 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileWithBrokenApplyBothUsage::class)] final class ProfileWithBrokenApplyBothUsage extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + #[Apply(ProfileCreated::class)] #[Apply] protected function applyProfileCreated(ProfileCreated|ProfileVisited $event): void { } - - public function aggregateRootId(): string - { - return self::class; - } } diff --git a/tests/Unit/Fixture/ProfileWithBrokenApplyIntersection.php b/tests/Unit/Fixture/ProfileWithBrokenApplyIntersection.php index cbd6e2c41..e09b1433e 100644 --- a/tests/Unit/Fixture/ProfileWithBrokenApplyIntersection.php +++ b/tests/Unit/Fixture/ProfileWithBrokenApplyIntersection.php @@ -7,17 +7,16 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileWithBrokenApplyIntersection::class)] final class ProfileWithBrokenApplyIntersection extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + #[Apply] protected function applyIntersection(ProfileCreated&ProfileVisited $event): void { } - - public function aggregateRootId(): string - { - return self::class; - } } diff --git a/tests/Unit/Fixture/ProfileWithBrokenApplyMultipleApply.php b/tests/Unit/Fixture/ProfileWithBrokenApplyMultipleApply.php index 50487d828..cd9e66ee9 100644 --- a/tests/Unit/Fixture/ProfileWithBrokenApplyMultipleApply.php +++ b/tests/Unit/Fixture/ProfileWithBrokenApplyMultipleApply.php @@ -7,18 +7,17 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileWithBrokenApplyMultipleApply::class)] final class ProfileWithBrokenApplyMultipleApply extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + #[Apply] #[Apply] protected function applyNameChanged(NameChanged $event): void { } - - public function aggregateRootId(): string - { - return self::class; - } } diff --git a/tests/Unit/Fixture/ProfileWithBrokenApplyNoType.php b/tests/Unit/Fixture/ProfileWithBrokenApplyNoType.php index 2720574d6..0352a72ff 100644 --- a/tests/Unit/Fixture/ProfileWithBrokenApplyNoType.php +++ b/tests/Unit/Fixture/ProfileWithBrokenApplyNoType.php @@ -7,18 +7,17 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileWithBrokenApplyNoType::class)] final class ProfileWithBrokenApplyNoType extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + /** @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint */ #[Apply] protected function applyWithNoType($event): void { } - - public function aggregateRootId(): string - { - return self::class; - } } diff --git a/tests/Unit/Fixture/ProfileWithEmptyApply.php b/tests/Unit/Fixture/ProfileWithEmptyApply.php index f3ffb8e9b..06b343108 100644 --- a/tests/Unit/Fixture/ProfileWithEmptyApply.php +++ b/tests/Unit/Fixture/ProfileWithEmptyApply.php @@ -7,10 +7,14 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate(ProfileWithEmptyApply::class)] final class ProfileWithEmptyApply extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + #[Apply] protected function applyProfileCreated(ProfileCreated|ProfileVisited $event): void { @@ -20,9 +24,4 @@ protected function applyProfileCreated(ProfileCreated|ProfileVisited $event): vo protected function applyNameChanged(NameChanged $event): void { } - - public function aggregateRootId(): string - { - return self::class; - } } diff --git a/tests/Unit/Fixture/ProfileWithSnapshot.php b/tests/Unit/Fixture/ProfileWithSnapshot.php index abb9606c7..3c0f65e5f 100644 --- a/tests/Unit/Fixture/ProfileWithSnapshot.php +++ b/tests/Unit/Fixture/ProfileWithSnapshot.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Apply; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\Snapshot; use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; use Patchlevel\Hydrator\Normalizer\ArrayNormalizer; @@ -17,6 +18,7 @@ final class ProfileWithSnapshot extends BasicAggregateRoot { #[ProfileIdNormalizer] + #[Id] private ProfileId $id; #[EmailNormalizer] private Email $email; @@ -73,9 +75,4 @@ protected function applyMessagePublished(MessagePublished $event): void { $this->messages[] = $event->message; } - - public function aggregateRootId(): string - { - return $this->id->toString(); - } } diff --git a/tests/Unit/Fixture/ProfileWithSuppressAll.php b/tests/Unit/Fixture/ProfileWithSuppressAll.php index 7800ab121..2f3d1ecc9 100644 --- a/tests/Unit/Fixture/ProfileWithSuppressAll.php +++ b/tests/Unit/Fixture/ProfileWithSuppressAll.php @@ -6,12 +6,16 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; +use Patchlevel\EventSourcing\Attribute\Id; use Patchlevel\EventSourcing\Attribute\SuppressMissingApply; #[Aggregate(ProfileWithSuppressAll::class)] #[SuppressMissingApply(SuppressMissingApply::ALL)] final class ProfileWithSuppressAll extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + public static function createProfile(ProfileId $id, Email $email): self { $self = new self(); @@ -19,9 +23,4 @@ public static function createProfile(ProfileId $id, Email $email): self return $self; } - - public function aggregateRootId(): string - { - return '1'; - } } diff --git a/tests/Unit/Fixture/WrongNormalizerBasicAggregate.php b/tests/Unit/Fixture/WrongNormalizerBasicAggregate.php index 232e8e0e9..b4b982c25 100644 --- a/tests/Unit/Fixture/WrongNormalizerBasicAggregate.php +++ b/tests/Unit/Fixture/WrongNormalizerBasicAggregate.php @@ -6,15 +6,14 @@ use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot; use Patchlevel\EventSourcing\Attribute\Aggregate; +use Patchlevel\EventSourcing\Attribute\Id; #[Aggregate('wrong_normalizer')] final class WrongNormalizerBasicAggregate extends BasicAggregateRoot { + #[Id] + private ProfileId $id; + #[EmailNormalizer] public bool $email = true; - - public function aggregateRootId(): string - { - return '1'; - } } diff --git a/tests/Unit/Repository/DefaultRepositoryTest.php b/tests/Unit/Repository/DefaultRepositoryTest.php index fe3d8ad39..5669b4c7e 100644 --- a/tests/Unit/Repository/DefaultRepositoryTest.php +++ b/tests/Unit/Repository/DefaultRepositoryTest.php @@ -527,7 +527,7 @@ public function testLoadAggregate(): void Profile::metadata(), ); - $aggregate = $repository->load('1'); + $aggregate = $repository->load(ProfileId::fromString('1')); self::assertInstanceOf(Profile::class, $aggregate); self::assertSame(1, $aggregate->playhead()); @@ -568,8 +568,8 @@ public function testLoadAggregateTwice(): void Profile::metadata(), ); - $aggregate1 = $repository->load('1'); - $aggregate2 = $repository->load('1'); + $aggregate1 = $repository->load(ProfileId::fromString('1')); + $aggregate2 = $repository->load(ProfileId::fromString('1')); self::assertEquals($aggregate1, $aggregate2); self::assertNotSame($aggregate1, $aggregate2); @@ -593,7 +593,7 @@ public function testAggregateNotFound(): void Profile::metadata(), ); - $repository->load('1'); + $repository->load(ProfileId::fromString('1')); } public function testHasAggregate(): void @@ -612,7 +612,7 @@ public function testHasAggregate(): void Profile::metadata(), ); - self::assertTrue($repository->has('1')); + self::assertTrue($repository->has(ProfileId::fromString('1'))); } public function testNotHasAggregate(): void @@ -631,13 +631,15 @@ public function testNotHasAggregate(): void Profile::metadata(), ); - self::assertFalse($repository->has('1')); + self::assertFalse($repository->has(ProfileId::fromString('1'))); } public function testLoadAggregateWithSnapshot(): void { + $id = ProfileId::fromString('1'); + $profile = ProfileWithSnapshot::createProfile( - ProfileId::fromString('1'), + $id, Email::fromString('hallo@patchlevel.de'), ); @@ -654,7 +656,7 @@ public function testLoadAggregateWithSnapshot(): void $snapshotStore = $this->prophesize(SnapshotStore::class); $snapshotStore->load( ProfileWithSnapshot::class, - '1', + $id, )->willReturn($profile); $repository = new DefaultRepository( @@ -664,7 +666,7 @@ public function testLoadAggregateWithSnapshot(): void $snapshotStore->reveal(), ); - $aggregate = $repository->load('1'); + $aggregate = $repository->load(ProfileId::fromString('1')); self::assertInstanceOf(ProfileWithSnapshot::class, $aggregate); self::assertSame(1, $aggregate->playhead()); @@ -706,8 +708,8 @@ public function testLoadAggregateWithSnapshotFirstTime(): void $snapshotStore = $this->prophesize(SnapshotStore::class); $snapshotStore->load( ProfileWithSnapshot::class, - '1', - )->willThrow(new SnapshotNotFound(ProfileWithSnapshot::class, '1')); + ProfileId::fromString('1'), + )->willThrow(new SnapshotNotFound(ProfileWithSnapshot::class, ProfileId::fromString('1'))); $snapshotStore->save(Argument::type(ProfileWithSnapshot::class))->shouldBeCalled(); @@ -718,7 +720,7 @@ public function testLoadAggregateWithSnapshotFirstTime(): void $snapshotStore->reveal(), ); - $aggregate = $repository->load('1'); + $aggregate = $repository->load(ProfileId::fromString('1')); self::assertInstanceOf(ProfileWithSnapshot::class, $aggregate); self::assertSame(3, $aggregate->playhead()); @@ -764,7 +766,7 @@ public function testLoadAggregateWithSnapshotAndSaveNewVersion(): void $snapshotStore = $this->prophesize(SnapshotStore::class); $snapshotStore->load( ProfileWithSnapshot::class, - '1', + ProfileId::fromString('1'), )->willReturn($profile); $snapshotStore->save($profile)->shouldBeCalled(); @@ -776,7 +778,7 @@ public function testLoadAggregateWithSnapshotAndSaveNewVersion(): void $snapshotStore->reveal(), ); - $aggregate = $repository->load('1'); + $aggregate = $repository->load(ProfileId::fromString('1')); self::assertInstanceOf(ProfileWithSnapshot::class, $aggregate); self::assertSame(4, $aggregate->playhead()); @@ -800,7 +802,7 @@ public function testLoadAggregateWithoutSnapshot(): void $eventBus = $this->prophesize(EventBus::class); $snapshotStore = $this->prophesize(SnapshotStore::class); - $snapshotStore->load(ProfileWithSnapshot::class, '1') + $snapshotStore->load(ProfileWithSnapshot::class, ProfileId::fromString('1')) ->willThrow(SnapshotNotFound::class); $repository = new DefaultRepository( @@ -810,7 +812,7 @@ public function testLoadAggregateWithoutSnapshot(): void $snapshotStore->reveal(), ); - $aggregate = $repository->load('1'); + $aggregate = $repository->load(ProfileId::fromString('1')); self::assertInstanceOf(ProfileWithSnapshot::class, $aggregate); self::assertSame(1, $aggregate->playhead()); diff --git a/tests/Unit/Serializer/Normalizer/IdNormalizerTest.php b/tests/Unit/Serializer/Normalizer/IdNormalizerTest.php new file mode 100644 index 000000000..8a24d8f07 --- /dev/null +++ b/tests/Unit/Serializer/Normalizer/IdNormalizerTest.php @@ -0,0 +1,61 @@ +assertEquals(null, $normalizer->normalize(null)); + } + + public function testDenormalizeWithNull(): void + { + $normalizer = new IdNormalizer(CustomId::class); + $this->assertEquals(null, $normalizer->denormalize(null)); + } + + public function testNormalizeWithInvalidArgument(): void + { + $this->expectException(InvalidArgument::class); + $this->expectExceptionMessage('type "Patchlevel\EventSourcing\Aggregate\CustomId" was expected but "string" was passed.'); + + $normalizer = new IdNormalizer(CustomId::class); + $normalizer->normalize('foo'); + } + + public function testDenormalizeWithInvalidArgument(): void + { + $this->expectException(InvalidUuidStringException::class); + + $normalizer = new IdNormalizer(Uuid::class); + $normalizer->denormalize('foo'); + } + + public function testNormalizeWithValue(): void + { + $normalizer = new IdNormalizer(CustomId::class); + $this->assertEquals('foo', $normalizer->normalize(new CustomId('foo'))); + } + + public function testDenormalizeWithValue(): void + { + $normalizer = new IdNormalizer(CustomId::class); + $this->assertEquals(new CustomId('foo'), $normalizer->denormalize('foo')); + } +} diff --git a/tests/Unit/Snapshot/DefaultSnapshotStoreTest.php b/tests/Unit/Snapshot/DefaultSnapshotStoreTest.php index dd656b994..9ba4fb179 100644 --- a/tests/Unit/Snapshot/DefaultSnapshotStoreTest.php +++ b/tests/Unit/Snapshot/DefaultSnapshotStoreTest.php @@ -56,7 +56,7 @@ public function testLoad(): void $store = new DefaultSnapshotStore(['memory' => $adapter->reveal()]); - $aggregate = $store->load(ProfileWithSnapshot::class, '1'); + $aggregate = $store->load(ProfileWithSnapshot::class, ProfileId::fromString('1')); self::assertEquals(ProfileId::fromString('1'), $aggregate->id()); self::assertEquals(Email::fromString('info@patchlevel.de'), $aggregate->email()); @@ -74,7 +74,7 @@ public function testLoadLegacySnapshots(): void $store = new DefaultSnapshotStore(['memory' => $adapter->reveal()]); - $store->load(ProfileWithSnapshot::class, '1'); + $store->load(ProfileWithSnapshot::class, ProfileId::fromString('1')); } public function testLoadExpiredSnapshot(): void @@ -93,7 +93,7 @@ public function testLoadExpiredSnapshot(): void $store = new DefaultSnapshotStore(['memory' => $adapter->reveal()]); - $store->load(ProfileWithSnapshot::class, '1'); + $store->load(ProfileWithSnapshot::class, ProfileId::fromString('1')); } public function testAdapterIsMissing(): void @@ -101,7 +101,7 @@ public function testAdapterIsMissing(): void $this->expectException(AdapterNotFound::class); $store = new DefaultSnapshotStore([]); - $store->load(ProfileWithSnapshot::class, '1'); + $store->load(ProfileWithSnapshot::class, ProfileId::fromString('1')); } public function testGetAdapter(): void