Skip to content

Repository

A repository takes care of storing and loading the aggregates. He is also responsible for building messages from the events and then dispatching them to the event bus.

Every aggregate needs a repository to be stored. And each repository is only responsible for one aggregate.

Create a repository

The best way to create a repository is to use the DefaultRepositoryManager. This helps to build the repository correctly.

The DefaultRepositoryManager needs some services to work. For one, it needs AggregateRootRegistry so that it knows which aggregates exist. The store, which is then given to the repository so that it can save and load the events at the end. And the EventBus to publish the new events.

After plugging the DefaultRepositoryManager together, you can create the repository associated with the aggregate.

use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;

$repositoryManager = new DefaultRepositoryManager(
    $aggregateRootRegistry,
    $store,
    $eventBus
);

$repository = $repositoryManager->get(Profile::class);

Note

The same repository instance is always returned for a specific aggregate.

Snapshots

Loading events for an aggregate is superfast. You can have thousands of events in the database that load in a few milliseconds and build the corresponding aggregate.

But at some point you realize that it takes time. To counteract this there is a snapshot store.

use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;
use Patchlevel\EventSourcing\Snapshot\Adapter\Psr16SnapshotAdapter;
use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore;

$adapter = new Psr16SnapshotAdapter($cache);
$snapshotStore = new DefaultSnapshotStore([
    'default' => $adapter
]);

$repositoryManager = new DefaultRepositoryManager(
    $aggregateRootRegistry,
    $store,
    $eventBus,
    $snapshotStore
);

$repository = $repositoryManager->get(Profile::class);

Note

You can find out more about snapshots here.

Decorator

If you want to add more metadata to the message, like e.g. an application id, then you can use decorator.

use Patchlevel\EventSourcing\EventBus\Decorator\RecordedOnDecorator;
use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager;

$decorator = new RecordedOnDecorator($clock);

$repositoryManager = new DefaultRepositoryManager(
    $aggregateRootRegistry,
    $store,
    $eventBus,
    null,
    $decorator
);

$repository = $repositoryManager->get(Profile::class);

Warning

We also use the decorator to fill in the recordedOn time. If you want to add your own decorator, then you need to make sure to add the RecordedOnDecorator as well. You can e.g. solve with the ChainMessageDecorator.

Note

You can find out more about message decorator here.

Use the repository

Each repository has three methods that are responsible for loading an aggregate, saving it or checking whether it exists.

Save an aggregate

An aggregate can be saved. All new events that have not yet been written to the database are fetched from the aggregate. These events are then also append to the database. After the events have been written, the new events are dispatched on the event bus.

$profile = Profile::create('[email protected]');

$repository->save($profile);

Note

All events are written to the database with one transaction in order to ensure data consistency.

Tip

If you want to make sure that dispatching events and storing events is transaction safe, then you should look at the outbox pattern.

Load an aggregate

An aggregate can be loaded using the load method. All events for the aggregate are loaded from the database and the current state is rebuilt.

$profile = $repository->load('229286ff-6f95-4df6-bc72-0a239fe7b284');

Warning

When the method is called, the aggregate is always reloaded from the database and rebuilt.

Note

You can only fetch one aggregate at a time and don't do any complex queries either. Projections are used for this purpose.

Has an aggregate

You can also check whether an aggregate with a certain id exists. It is checked whether any event with this id exists in the database.

if($repository->has('229286ff-6f95-4df6-bc72-0a239fe7b284')) {
    // ...
}

Note

The query is fast and does not load any event. This means that the state of the aggregate is not rebuild either.

Custom Repository

In clean code you want to have explicit type hints for the repositories so that you don't accidentally use the wrong repository. It would also help in frameworks with a dependency injection container, as this allows the services to be autowired. However, you cannot inherit from our repository implementations. Instead, you just have to wrap these repositories. This also gives you more type security.

use Patchlevel\EventSourcing\Repository\Repository;
use Patchlevel\EventSourcing\Repository\RepositoryManager;

class ProfileRepository 
{
    /** @var Repository<Profile>  */
    private Repository $repository;

    public function __construct(RepositoryManager $repositoryManager) 
    {
        $this->repository = $repositoryManager->get(Profile::class);
    }

    public function load(ProfileId $id): Profile 
    {
        return $this->repository->load($id->toString());
    }

    public function save(Profile $profile): void 
    {
        return $this->repository->save($profile);
    }

    public function has(ProfileId $id): bool 
    {
        return $this->repository->has($id->toString());
    }
}