-
Notifications
You must be signed in to change notification settings - Fork 7
Description
We sometimes have Aggregates that do not have an explicit create
event, they just come into existence by whatever event is recorded first (often this is some ID managed outside the context of that particular Aggregate, e.g. some external IDs like users coming from external IDPs).
Right now we have to create a custom repository with a load
function somewhat like this:
public function load(Uuid $aggregateId): MyAggregate
{
return $this->repository->has($aggregateId)
? $this->repository->load($aggregateId)
: MyAggregate::create($aggregateId);
}
As we cannot define our own repository (easily) to be used by the DefaultRepositoryMananger
, we loose the benefits of defining #[Handler]
directly in the aggregate and all the loading/saving coming with it. Instead we have to go manual and create custom handler classes for all commands of this aggregate (using Symfony):
#[AsMessageHandler]
public function myCommandHanlder(MyCommand $command): void
{
$aggregate = $this->repository->load($command->id);
$aggregate->myCommand($command);
$this->repository->save($aggregate);
}
As I see it, this could be solved by two means:
- Allow extension of the default repository class, and extend the
Aggregate
attribute to allow specification of the repository class to use. This is what Doctrine does with their entity repositories:#[Entity(repositoryClass: MyRepository::class)
and would tie nicely with the repository manager. E.g.$repositoryManager->get(MyAggregate::class);
would return the custom implementation specified in the attribute. Note that doctrine bundle also just uses a proxy for the actual implementation for convenience of passing arguments. - The alternative would be to add an optional argument
allow_implicit_create
argument to theAggregate
attribute. TheDefaultRepository::load
method already knows about theId
property, so it could instantiate a new aggregate instance and set the ID to return an empty aggregate root.
Personally I prefer the first option - it leaves more room for extension than just pilling flags on the Aggregate
that inherently should affect the repository, not the aggregate. Using the SymfonyBundle, this currently would entail to overwrite the DefaultRepositoryManager
.
I guess this would mean to:
- extend the
AggregateRootMetadata
with an additional property for the repository class - extend the
DefaultRepositoryManager
to check for the property instead of instantiating the default repository - find some pattern to hide away all the extra parameters the
DefaultRepository
does need
This is the current implementation we use by decorating the RepositoryManager
(simply using a static array of aggregates to handle different):
#[AsDecorator(decorates: DefaultRepositoryManager::class)]
class ImplicitCreateRepositoryManager implements RepositoryManager
{
private static array $implicitlyCreatedAggregates = [
FeatureFlagOverwrite::class => true,
];
public function __construct(
private readonly RepositoryManager $repositoryManager,
private readonly AggregateRootMetadataFactory $metadataFactory,
) {
}
public function get(string $aggregateClass): Repository
{
$repository = $this->repositoryManager->get($aggregateClass);
return array_key_exists($aggregateClass, self::$implicitlyCreatedAggregates)
? new ImplicitCreateRepository($repository, $this->metadataFactory->metadata($aggregateClass))
: $repository;
}
}
class ImplicitCreateRepository implements Repository
{
public function __construct(
private readonly Repository $repository,
private readonly AggregateRootMetadata $metadata,
) {
}
public function load(AggregateRootId $id): AggregateRoot
{
if ($this->repository->has($id)) {
return $this->repository->load($id);
}
$reflectionClass = new ReflectionClass($this->metadata->className);
$aggregate = $reflectionClass->newInstanceWithoutConstructor();
$reflectionClass
->getProperty($this->metadata->idProperty)
->setValue($aggregate, $id);
return $aggregate;
}
public function has(AggregateRootId $id): bool
{
return $this->repository->has($id);
}
public function save(AggregateRoot $aggregate): void
{
$this->repository->save($aggregate);
}
}