diff --git a/src/AggregateRoots/AggregatePartial.php b/src/AggregateRoots/AggregatePartial.php index a7ab48f8..3af44af1 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; @@ -35,6 +37,24 @@ 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 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 c5d25cad..242b78d7 100644 --- a/src/AggregateRoots/AggregateRoot.php +++ b/src/AggregateRoots/AggregateRoot.php @@ -193,13 +193,37 @@ protected function getState(): array ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->isStatic()) ->mapWithKeys(function (ReflectionProperty $property) { return [$property->getName() => $this->{$property->getName()}]; - })->toArray(); + }) + ->merge($this->getPartialsState()) + ->toArray(); + } + + protected function getPartialsState(): array + { + $partials = []; + foreach ($this->resolvePartials() as $partial) { + $partials[$partial::class] = $partial->getState(); + } + return $partials; + } + + 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) { - $this->$key = $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 92e96335..5ddb6e6f 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; @@ -168,6 +171,22 @@ }); }); +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) { + assertEquals(300, $snapshot->state[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); @@ -204,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); 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); + } + +}