CodefyPHP FrameworkCodefyPHP Framework
  • Home
  • Community
    • Forum
    • Github
    • YouTube
  • News
  • Home
  • Community
    • Forum
    • Github
    • YouTube
  • News
home/Knowledge Base/Aggregates/Event Sourcing
Popular Search:installation, codex, mail

Event Sourcing

58 views 0

Written by Joshua
August 14, 2024

Up to this point, we’ve kept our Post aggregate in memory. In the end, we will want to persist it and load it back into memory for later use. Loading an aggregate back into memory involves reconstituting its previously recorded history. This concept is called Event Sourcing. The events are the single source of reference that make up the aggregate.

TDD

Let’s write out a test when events are stored and then retrieved from the store.

<?php

use App\Domain\Post\Post;
use App\Domain\Post\ValueObject\PostId;
use App\Domain\Post\ValueObject\Title;
use App\Domain\Post\ValueObject\Content;
use Codefy\Domain\Aggregate\EventStream;

use function expect;
use function it;
use function iterator_to_array;

it('should be the same after reconstitution.', function () {
    $postId = new PostId(value: '1cf57c2c-5c82-45a0-8a42-f0b725cfc42f');
    $post = Post::createPost(
        postId: $postId,
        title: new Title(value: 'Second Post Title'),
        content: new Content(value: 'Another short form content.')
    );
    $post->changeTitle(title: new Title(value: 'Reconstitute From History'));
    $events = $post->getRecordedEvents();
    $post->clearRecordedEvents();

    $reconstitutedPost = Post::reconstituteFromEventStream(
        aggregateHistory: new EventStream(
            aggregateId: $postId,
            events: iterator_to_array(
                iterator: $events->getIterator()
            )
        )
    );

    expect(value: $post)->toEqual(expected: $reconstitutedPost);
});

Implement Interface

We need to start updating our Post aggregate so that it can now be event sourced.

<?php

declare(strict_types=1);

namespace App\Domain\Post;

use App\Domain\Post\Event\PostWasCreated;
use App\Domain\Post\Event\TitleWasChanged;
use App\Domain\Post\Exceptions\TitleWasNullException;
use BadMethodCallException;
use Codefy\Domain\Aggregate\EventStream;
use Codefy\Domain\Aggregate\IsEventSourced;
use Codefy\Domain\Aggregate\RecordsEvents;
use Codefy\Domain\EventSourcing\DomainEvent;
use Codefy\Domain\EventSourcing\DomainEvents;
use ReflectionClass;

use function method_exists;
use function sprintf;

final class Post implements RecordsEvents, EventSourcing
{
}

We declare that Post isEventSourced and that it can be rebuilt from events.

Reconstitute

The isEventSourced interface requires us to implement the static method, reconstituteFromEventStream(EventStream $aggregateHistory).

<?php

public static function reconstituteFromEventStream(EventStream $aggregateHistory): RecordsEvents
{
    $postId = $aggregateHistory->aggregateId();
    $post = new Post(new PostId($postId->__toString()));

    foreach ($aggregateHistory as $event) {
        $post->when($event);
    }
    
    return $post;
}

Reconstitution implies that Post exists in some form and that it can be retrieved. The EventStream is a list of chronological (very important) DomainEvents. We start off by fetching the aggregate’s identifier: $postId. Then we instantiate the Post aggregate and remember the constructor is private.

Apply Method

Now we need to iterate through the historical events and rebuild the aggregate’s state until we get to the current state. We cannot the methods createPost or changeTitle, otherwise, we will be calling the record method, causing events to be recorded a second time.

Instead, we need a new private method called when. The method when helps apply the events instead of record them. Therefore, our aggregate needs a couple of new methods for this to work. Since our aggregate records two events named PostWasCreated and TitleWasChanged, the when method will automatically look for methods whenPostWasCreated and whenTitleWasChanged. These methods manipulate state.

<?php

protected function whenPostWasCreated(PostWasCreated $event): void
{
    $this->postId = $event->aggregateId();
    $this->title = $event->title();
    $this->content = $event->content();
}

protected function whenTitleWasChanged(TitleWasChanged $event): void
{
    $this->postId = $event->aggregateId();
    $this->title = $event->title();
}

Update record Method

Lastly, we need to make sure that newly recorded events are also applied to state. Therefore, we need to update our record method:

<?php

protected function recordThat(DomainEvent $event): void
{
    $this->recordedEvents[] = $event;

    $this->when($event);
}

Below is the new state of our aggregate class:

<?php

final class Post implements RecordsEvents, IsEventSourced
{
    protected array $recordedEvents = [];

    private function __construct(private PostId $postId)
    {
    }

    /**
     * @throws TitleWasNullException
     */
    public static function createPost(PostId $postId, Title $title, Content $content): Post
    {
        if ($title->isEmpty()) {
            throw new TitleWasNullException(message: 'Title cannot be null.');
        }

        $post = self::root(aggregateId: $postId);

        $post->recordApplyAndPublishThat(
            event: PostWasCreated::withData($postId, $title, $content)
        );

        return $post;
    }

    /**
     * @throws TitleWasNullException
     */
    public function changeTitle(Title $title): void
    {
        if ($title->isEmpty()) {
            throw new TitleWasNullException(message: 'Title cannot be null.');
        }
        if ($title->__toString() === $this->title->__toString()) {
            return;
        }
        $this->recordApplyAndPublishThat(
            event: TitleWasChanged::withData(postId: $this->postId, title: $title)
        );
    }

    private function recordThat(DomainEvent $event): void
    {
        $this->recordedEvents[] = $event;

        $this->when($event);
    }

    protected function when(DomainEvent $event): void
    {
        $method = sprintf('when%s', (new ReflectionClass($event))->getShortName());

        if (!method_exists($this, $method)) {
            throw new BadMethodCallException(
                sprintf("There is no method named '%s' that can be called in '%s'.", $method, static::class)
            );
        }

        $this->$method($event);
    }

    public function hasRecordedEvents(): bool
    {
        return !empty($this->recordedEvents);
    }

    public function getRecordedEvents(): DomainEvents
    {
        return DomainEvents::fromArray(events: $this->recordedEvents);
    }

    public function clearRecordedEvents()
    {
        $this->recordedEvents = [];
    }
}

Github

An advanced version of this Post aggregate can be found on Github.

Forum

If you have any questions or issues, please feel free to post to the Documentation Forum.

SLA Support

If you are needing more hands on support, needing consultation, or help with setup, support me on Github at $60 or more. Once you've sponsored me, you will receive an email on the best way to contact me to start your support.

Edit on Github

Last Updated on August 14, 2024 by Joshua

Related Articles
  • Aggregate Repository
  • Event Store
  • Protecting Invariants
  • Value Objects
  • Records Events
  • Identifies Aggregate

Didn't find your answer? Check out the Forum

  Protecting Invariants

Event Store  

  • Copyright 2025 CodefyPHP.com. All Rights Reserved

Popular Search:installation, codex, mail