From eebc49360c1469563324b9cc6cf164e0fdf119f4 Mon Sep 17 00:00:00 2001 From: SonjaTuro Date: Sun, 23 Apr 2023 17:05:43 +1000 Subject: [PATCH 1/5] Store partials into snapshot --- src/AggregateRoots/AggregatePartial.php | 13 +++++ src/AggregateRoots/AggregateRoot.php | 15 +++++- tests/AggregateRootTest.php | 20 ++++++++ .../AccountAggregateRootWithPartial.php | 46 ++++++++++++++++++ .../AggregateRoots/Partials/MoneyPartial.php | 47 +++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/TestClasses/AggregateRoots/AccountAggregateRootWithPartial.php create mode 100644 tests/TestClasses/AggregateRoots/Partials/MoneyPartial.php diff --git a/src/AggregateRoots/AggregatePartial.php b/src/AggregateRoots/AggregatePartial.php index 6856d348..8b0f428f 100644 --- a/src/AggregateRoots/AggregatePartial.php +++ b/src/AggregateRoots/AggregatePartial.php @@ -3,6 +3,8 @@ namespace Spatie\EventSourcing\AggregateRoots; use Ramsey\Uuid\Uuid; +use ReflectionClass; +use ReflectionProperty; use Spatie\EventSourcing\EventHandlers\AppliesEvents; use Spatie\EventSourcing\StoredEvents\ShouldBeStored; use Spatie\EventSourcing\StoredEvents\StoredEvent; @@ -33,6 +35,17 @@ public function apply(StoredEvent | ShouldBeStored ...$storedEvents): void } } + public function getState(): array + { + $class = new ReflectionClass($this); + + return collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) + ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->isStatic()) + ->mapWithKeys(function (ReflectionProperty $property) { + return [$property->getName() => $this->{$property->getName()}]; + })->toArray(); + } + public static function fake(): static { $aggregateRoot = FakeAggregateRootForPartial::retrieve(Uuid::uuid4()->toString()); diff --git a/src/AggregateRoots/AggregateRoot.php b/src/AggregateRoots/AggregateRoot.php index f7535937..898a7e2a 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -34,6 +34,8 @@ abstract class AggregateRoot protected int $aggregateVersionAfterReconstitution = 0; + protected static string $partialsKey = '__esPartials'; + /** @var \Illuminate\Support\Collection|\Spatie\EventSourcing\AggregateRoots\AggregatePartial[] */ protected Collection $entities; @@ -191,7 +193,18 @@ protected function getState(): array ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->isStatic()) ->mapWithKeys(function (ReflectionProperty $property) { return [$property->getName() => $this->{$property->getName()}]; - })->toArray(); + }) + ->put(self::$partialsKey, $this->getPartialsState()) + ->toArray(); + } + + protected function getPartialsState(): array + { + $partials = []; + foreach ($this->resolvePartials() as $partial) { + $partials[$partial::class] = $partial->getState(); + } + return $partials; } protected function useState(array $state): void diff --git a/tests/AggregateRootTest.php b/tests/AggregateRootTest.php index 15ed9511..816dd4db 100644 --- a/tests/AggregateRootTest.php +++ b/tests/AggregateRootTest.php @@ -12,6 +12,7 @@ use function PHPUnit\Framework\assertEmpty; use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertNotEmpty; use function PHPUnit\Framework\assertTrue; use Spatie\EventSourcing\AggregateRoots\AggregateRoot; @@ -22,9 +23,11 @@ use Spatie\EventSourcing\StoredEvents\Models\EloquentStoredEvent; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRoot; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRootWithFailingPersist; +use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRootWithPartial; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRootWithStoredEventRepositorySpecified; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Mailable\MoneyAddedMailable; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Math; +use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Partials\MoneyPartial; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Projectors\AccountProjector; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Reactors\DoubleBalanceReactor; use Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\Reactors\SendMailReactor; @@ -167,6 +170,23 @@ }); }); +it('should store partial states when snapshotting', function () { + /** @var \Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRoot $aggregateRoot */ + $aggregateRoot = AccountAggregateRootWithPartial::retrieve($this->aggregateUuid); + + $aggregateRoot + ->addMoney(100) + ->addMoney(100) + ->addMoney(100); + + $aggregateRoot->snapshot(); + + tap(EloquentSnapshot::first(), function (EloquentSnapshot $snapshot) { + assertCount(1, $snapshot->state['__esPartials']); + assertEquals(300, $snapshot->state['__esPartials'][MoneyPartial::class]['balance']); + }); +}); + it('should restore public properties when restoring an aggregate root with a snapshot', function () { /** @var \Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRoot $aggregateRoot */ $aggregateRoot = AccountAggregateRoot::retrieve($this->aggregateUuid); diff --git a/tests/TestClasses/AggregateRoots/AccountAggregateRootWithPartial.php b/tests/TestClasses/AggregateRoots/AccountAggregateRootWithPartial.php new file mode 100644 index 00000000..41394b66 --- /dev/null +++ b/tests/TestClasses/AggregateRoots/AccountAggregateRootWithPartial.php @@ -0,0 +1,46 @@ +dependency = $dependency; + $this->math = $math; + + $this->moneyPartial = new MoneyPartial($this, $math); + } + + public function addMoney(int $amount): self + { + $this->moneyPartial->addMoney($amount); + + return $this; + } + + public function multiplyMoney(int $amount): self + { + $this->moneyPartial->multiplyMoney($amount); + + return $this; + } + + public function getBalance() + { + return $this->moneyPartial->balance; + } +} diff --git a/tests/TestClasses/AggregateRoots/Partials/MoneyPartial.php b/tests/TestClasses/AggregateRoots/Partials/MoneyPartial.php new file mode 100644 index 00000000..be52af0f --- /dev/null +++ b/tests/TestClasses/AggregateRoots/Partials/MoneyPartial.php @@ -0,0 +1,47 @@ +math = $math; + } + + public function addMoney(int $amount): self + { + $this->recordThat(new MoneyAdded($amount)); + + return $this; + } + + public function multiplyMoney(int $amount): self + { + $this->recordThat(new MoneyMultiplied($amount)); + + return $this; + } + + protected function applyMoneyAdded(MoneyAdded $event) + { + $this->balance += $event->amount; + } + + public function applyMoneyMultiplied(MoneyMultiplied $event) + { + $this->balance = $this->math->multiply($this->balance, $event->amount); + } + +} From 11f1e68e9015a84b6829d18c717b022df96f4e0c Mon Sep 17 00:00:00 2001 From: SonjaTuro Date: Sun, 23 Apr 2023 17:30:33 +1000 Subject: [PATCH 2/5] Add reconsitution of partials --- src/AggregateRoots/AggregatePartial.php | 7 +++++++ src/AggregateRoots/AggregateRoot.php | 12 +++++++++++- tests/AggregateRootTest.php | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/AggregateRoots/AggregatePartial.php b/src/AggregateRoots/AggregatePartial.php index 8b0f428f..c3e0ea6f 100644 --- a/src/AggregateRoots/AggregatePartial.php +++ b/src/AggregateRoots/AggregatePartial.php @@ -46,6 +46,13 @@ public function getState(): array })->toArray(); } + public function useState(array $state): void + { + foreach ($state as $key => $value) { + $this->$key = $value; + } + } + public static function fake(): static { $aggregateRoot = FakeAggregateRootForPartial::retrieve(Uuid::uuid4()->toString()); diff --git a/src/AggregateRoots/AggregateRoot.php b/src/AggregateRoots/AggregateRoot.php index 898a7e2a..848ad8d2 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -210,7 +210,17 @@ protected function getPartialsState(): array protected function useState(array $state): void { foreach ($state as $key => $value) { - $this->$key = $value; + if ($key === self::$partialsKey) { + foreach ($value as $partialKey => $partialState) { + foreach ($this->resolvePartials() as $partial) { + if ($partial::class === $partialKey) { + $partial->useState($partialState); + } + } + } + } else { + $this->$key = $value; + } } } diff --git a/tests/AggregateRootTest.php b/tests/AggregateRootTest.php index 816dd4db..30b2a988 100644 --- a/tests/AggregateRootTest.php +++ b/tests/AggregateRootTest.php @@ -223,6 +223,25 @@ assertEquals(400, $aggregateRootRetrieved->balance); }); +it('should have partials reconstituted if present', function () { + /** @var \Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRootWithPartial $aggregateRoot */ + $aggregateRoot = AccountAggregateRootWithPartial::retrieve($this->aggregateUuid); + + $aggregateRoot + ->addMoney(100) + ->addMoney(100) + ->addMoney(100) + ->persist(); + + $aggregateRoot->snapshot(); + $aggregateRoot->addMoney(100)->persist(); + + $aggregateRootRetrieved = AccountAggregateRootWithPartial::retrieve($this->aggregateUuid); + + assertEquals(4, $aggregateRootRetrieved->aggregateVersion); + assertEquals(400, $aggregateRootRetrieved->getBalance()); +}); + it('should replay all events in the correct order when retrieving an aggregate root all', function () { /** @var \Spatie\EventSourcing\Tests\TestClasses\AggregateRoots\AccountAggregateRoot $aggregateRoot */ $aggregateRoot = AccountAggregateRoot::retrieve($this->aggregateUuid); From 3ca901356df7efa23f8f5ed9cdc60e8203ba2770 Mon Sep 17 00:00:00 2001 From: SonjaTuro Date: Sun, 23 Apr 2023 17:58:32 +1000 Subject: [PATCH 3/5] Make partials key configurable --- config/event-sourcing.php | 7 +++++++ src/AggregateRoots/AggregateRoot.php | 6 ++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config/event-sourcing.php b/config/event-sourcing.php index 0013dbbe..61191b7f 100644 --- a/config/event-sourcing.php +++ b/config/event-sourcing.php @@ -75,6 +75,13 @@ */ 'snapshot_model' => Spatie\EventSourcing\Snapshots\EloquentSnapshot::class, + /** + * The key within an AggregateRoot under which snapshots of partials are stored. + * This is stored within the Aggregate state when snapshotted, and restored when + * a snapshot is retrieved. + */ + 'snapshot_partials_key' => '__esPartials', + /* * This class is responsible for handling stored events. To add extra behaviour you * can change this to a class of your own. The only restriction is that diff --git a/src/AggregateRoots/AggregateRoot.php b/src/AggregateRoots/AggregateRoot.php index 848ad8d2..f73d6b6a 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -34,8 +34,6 @@ abstract class AggregateRoot protected int $aggregateVersionAfterReconstitution = 0; - protected static string $partialsKey = '__esPartials'; - /** @var \Illuminate\Support\Collection|\Spatie\EventSourcing\AggregateRoots\AggregatePartial[] */ protected Collection $entities; @@ -194,7 +192,7 @@ protected function getState(): array ->mapWithKeys(function (ReflectionProperty $property) { return [$property->getName() => $this->{$property->getName()}]; }) - ->put(self::$partialsKey, $this->getPartialsState()) + ->put(config('event-sourcing.snapshot_partials_key'), $this->getPartialsState()) ->toArray(); } @@ -210,7 +208,7 @@ protected function getPartialsState(): array protected function useState(array $state): void { foreach ($state as $key => $value) { - if ($key === self::$partialsKey) { + if ($key === config('event-sourcing.snapshot_partials_key')) { foreach ($value as $partialKey => $partialState) { foreach ($this->resolvePartials() as $partial) { if ($partial::class === $partialKey) { From aed49a31bbb267912ef2f5c59137b72b6d0bd9d0 Mon Sep 17 00:00:00 2001 From: SonjaTuro Date: Sun, 23 Apr 2023 18:12:54 +1000 Subject: [PATCH 4/5] Function cleanup --- src/AggregateRoots/AggregateRoot.php | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/AggregateRoots/AggregateRoot.php b/src/AggregateRoots/AggregateRoot.php index f73d6b6a..9bce70c8 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -205,17 +205,27 @@ protected function getPartialsState(): array return $partials; } + protected function restorePartialsState(array $partialSnapshots): void + { + foreach ($partialSnapshots as $partialKey => $partialState) { + $this->restorePartialState($partialKey, $partialState); + } + } + + protected function restorePartialState(string $key, array $state): void + { + foreach ($this->resolvePartials() as $partial) { + if ($partial::class === $key) { + $partial->useState($state); + } + } + } + protected function useState(array $state): void { foreach ($state as $key => $value) { if ($key === config('event-sourcing.snapshot_partials_key')) { - foreach ($value as $partialKey => $partialState) { - foreach ($this->resolvePartials() as $partial) { - if ($partial::class === $partialKey) { - $partial->useState($partialState); - } - } - } + $this->restorePartialsState($value); } else { $this->$key = $value; } From f70ffdf561d52b0627104da12771cb3ec4cb8a85 Mon Sep 17 00:00:00 2001 From: SonjaTuro Date: Sat, 26 Aug 2023 11:37:13 +1000 Subject: [PATCH 5/5] Remove unneeded _esPartials key --- config/event-sourcing.php | 7 ------- src/AggregateRoots/AggregateRoot.php | 13 +++---------- tests/AggregateRootTest.php | 3 +-- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/config/event-sourcing.php b/config/event-sourcing.php index 61191b7f..0013dbbe 100644 --- a/config/event-sourcing.php +++ b/config/event-sourcing.php @@ -75,13 +75,6 @@ */ 'snapshot_model' => Spatie\EventSourcing\Snapshots\EloquentSnapshot::class, - /** - * The key within an AggregateRoot under which snapshots of partials are stored. - * This is stored within the Aggregate state when snapshotted, and restored when - * a snapshot is retrieved. - */ - 'snapshot_partials_key' => '__esPartials', - /* * This class is responsible for handling stored events. To add extra behaviour you * can change this to a class of your own. The only restriction is that diff --git a/src/AggregateRoots/AggregateRoot.php b/src/AggregateRoots/AggregateRoot.php index 9bce70c8..9968595a 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -192,7 +192,7 @@ protected function getState(): array ->mapWithKeys(function (ReflectionProperty $property) { return [$property->getName() => $this->{$property->getName()}]; }) - ->put(config('event-sourcing.snapshot_partials_key'), $this->getPartialsState()) + ->merge($this->getPartialsState()) ->toArray(); } @@ -205,13 +205,6 @@ protected function getPartialsState(): array return $partials; } - protected function restorePartialsState(array $partialSnapshots): void - { - foreach ($partialSnapshots as $partialKey => $partialState) { - $this->restorePartialState($partialKey, $partialState); - } - } - protected function restorePartialState(string $key, array $state): void { foreach ($this->resolvePartials() as $partial) { @@ -224,8 +217,8 @@ protected function restorePartialState(string $key, array $state): void protected function useState(array $state): void { foreach ($state as $key => $value) { - if ($key === config('event-sourcing.snapshot_partials_key')) { - $this->restorePartialsState($value); + if (class_exists($key)) { + $this->$key = $this->restorePartialState($key, $value); } else { $this->$key = $value; } diff --git a/tests/AggregateRootTest.php b/tests/AggregateRootTest.php index 30b2a988..a3f9c4cd 100644 --- a/tests/AggregateRootTest.php +++ b/tests/AggregateRootTest.php @@ -182,8 +182,7 @@ $aggregateRoot->snapshot(); tap(EloquentSnapshot::first(), function (EloquentSnapshot $snapshot) { - assertCount(1, $snapshot->state['__esPartials']); - assertEquals(300, $snapshot->state['__esPartials'][MoneyPartial::class]['balance']); + assertEquals(300, $snapshot->state[MoneyPartial::class]['balance']); }); });