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)
{
}
}