-
Notifications
You must be signed in to change notification settings - Fork 9
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
Aggregateattribute 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_createargument to theAggregateattribute. TheDefaultRepository::loadmethod already knows about theIdproperty, 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
AggregateRootMetadatawith an additional property for the repository class - extend the
DefaultRepositoryManagerto check for the property instead of instantiating the default repository - find some pattern to hide away all the extra parameters the
DefaultRepositorydoes 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);
}
}