Skip to content

Aggregate repository

Since our domain will have multiple Post instances, they will need to be collected into an AggregateRepository.

TDD

But first, let’s write out our tests:

<?php

declare(strict_types=1);

use App\Domain\Post\Post;
use App\Domain\Post\Repository\PostRepository;
use App\Domain\Post\ValueObject\PostId;
use App\Domain\Post\ValueObject\Title;
use App\Domain\Post\ValueObject\Content;
use Codefy\Domain\EventSourcing\InMemoryEventStore;
use Codefy\Tests\Domain\InMemoryPostProjection;
use PHPUnit\Framework\Assert;

use function expect;
use function it;

it('should reconstitute a Post to its state after persisting it.', function () {
    $postId = new PostId(value: '01K5EJ40GKE3G9WEZAH277N1AY');

    $post = Post::createPost(
        postId: $postId,
        title: new Title(value: 'Second Post Title'),
        content: new Content(value: 'Another short form content.')
    );

    $posts = new PostRepository(
        eventStore: new InMemoryEventStore(),
        projection: new InMemoryPostProjection()
    );

    $posts->saveAggregateRoot(aggregate: $post);

    $reconstitutedPost = $posts->loadAggregateRoot(aggregateId: $postId);

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

Implementation

<?php

declare(strict_types=1);

namespace App\Infrastructure\Persistence\Repository

use App\Domain\Post\Post;
use Codefy\Domain\Aggregate\AggregateId;
use Codefy\Domain\Aggregate\AggregateNotFoundException;
use Codefy\Domain\Aggregate\AggregateRepository;
use Codefy\Domain\Aggregate\RecordsEvents;
use Codefy\Domain\EventSourcing\EventStore;
use Codefy\Domain\EventSourcing\Projection;

final class PostRepository implements AggregateRepository
{
    public function __construct(public readonly EventStore $eventStore, public readonly Projection $projection)
    {
    }

    /**
     * Record the aggregate's latest events to
     * the event store.
     *
     * @param RecordsEvents $aggregate
     * @return void
     */
    public function saveAggregateRoot(RecordsEvents $aggregate): void
    {
        $events = $aggregate->getRecordedEvents();

        $this->eventStore->commit(events: $events);

        $aggregate->clearRecordedEvents();
    }

    /**
     * Fetch a single Post.
     *
     * @param AggregateId $aggregateId
     * @return RecordsEvents
     * @throws AggregateNotFoundException
     */
    public function loadAggregateRoot(AggregateId $aggregateId): RecordsEvents
    {
        $aggregateHistory = $this->eventStore->getAggregateHistoryFor(aggregateId: $aggregateId);
        return Post::reconstituteFromEventStream(aggregateHistory: $aggregateHistory);
    }
}

Now with our repository implemented, our tests should pass.

An example of a complete implementation of PostRepository can be found on Github.

You can use that as an example for an implementation of Codefy\Domain\Aggregate\AggregateRepository, or you can use the Codefy\Traits\EventSourcedRepositoryAware trait which implements the methods saveAggregateRoot and loadAggregateRoot.

<?php

declare(strict_types=1);

namespace App\Infrastructure\Persistence\Repository

use App\Domain\Post\Post;
use Codefy\Domain\Aggregate\AggregateId;
use Codefy\Domain\Aggregate\AggregateNotFoundException;
use Codefy\Domain\Aggregate\AggregateRepository;
use Codefy\Domain\Aggregate\RecordsEvents;
use Codefy\Domain\EventSourcing\EventStore;
use Codefy\Domain\EventSourcing\Projection;
use Codefy\Traits\EventSourcedRepositoryAware;

final class PostRepository implements AggregateRepository
{
    use EventSourcedRepositoryAware;

    public function __construct(public readonly EventStore $eventStore, public readonly Projection $projection)
    {
    }
}

Instead of using Codefy\Domain\EventSourcing\Projection as a type-hint, it is better to use an explicit interface to type-hint against when you add the alias in config/commandbus.php:

<?php

declare(strict_types=1);

namespace App\Domain\Post\Services;

use Codefy\Domain\EventSourcing\Projection;

interface PostProjection extends Projection
{
}

Then you can type-hint against an implementation of PostProjection, since you will need an implementation for each aggregate:

<?php

declare(strict_types=1);

namespace App\Infrastructure\Persistence\Repository;

use App\Domain\Post\Post;
use App\Domain\Post\Services;
use Codefy\Domain\Aggregate\AggregateId;
use Codefy\Domain\Aggregate\AggregateNotFoundException;
use Codefy\Domain\Aggregate\AggregateRepository;
use Codefy\Domain\Aggregate\RecordsEvents;
use Codefy\Domain\EventSourcing\EventStore;
use Codefy\Domain\EventSourcing\Projection;
use Codefy\Traits\EventSourcedRepositoryAware;

final class PostRepository implements AggregateRepository
{
    use EventSourcedRepositoryAware;

    public function __construct(public readonly EventStore $eventStore, public readonly PostProjection $projection)
    {
    }
}