-
Notifications
You must be signed in to change notification settings - Fork 11
Description
This is a screenshot from "The absolute beginner’s guide to DDD with Symfony" by @nealio82
Slides: https://speakerdeck.com/nealio82/the-absolute-beginners-guide-to-ddd-with-symfony?slide=105
My question was why the Domain Service slotConfirmationService was called inside the Command Handler instead of passing it to the named constructor of the Aggregate Root.
Our structure and code look very similar. Question about the domain service:
In similar use cases our application handler stays a pure orchestrator and instead would pass the service to the named constructor of the aggregate root. Thoughts?
Came from: https://twitter.com/webdevilopers/status/1426184442301800448
Current thread: https://twitter.com/webdevilopers/status/1426854214253400064
We had a similar use case a long time ago where we had to prevent employment contracts to overlap.
Those days we used the same approach calling a responsible unit in the handler. We did not have a service for it yet and used a repository and a separate Policy / Specification instead. Of course both could be moved to single service.
final class SignEmploymentContractHandler
{
public function __invoke(SignEmploymentContract $command): void
{
$person = $this->personDetailsRepository->ofPersonId($command->personId()->toString());
if (null === $person) {
throw new \Exception('Person not found.');
}
/** @var ContractDetails[] $enteredContracts */
$enteredContracts = $this->contractsDetailsRepository->ofPersonId($person->personId());
if (!OverlappingEmploymentContractPolicy::isSatisfiedBy(
$command->contractId(), $command->contractType(),
$command->employmentPeriod(), $command->employerId(), $enteredContracts
)) {
throw new EmploymentPeriodOverlapsException();
}
// ...
$contract = EmploymentContract::sign(/*...*/);
$this->contractCollection->save($contract);
}
}In new projects we would prefer this approach:
final class ContractPeriodConfirmationService
{
public function confirm(PersonId $personId, EmploymentPeriod $period, /*...*/)
{
$person = $this->personDetailsRepository->ofPersonId($command->personId()->toString());
if (null === $person) {
throw new \Exception('Person not found.');
}
/** @var ContractDetails[] $enteredContracts */
$enteredContracts = $this->contractsDetailsRepository->ofPersonId($person->personId());
if (!OverlappingEmploymentContractPolicy::isSatisfiedBy(
$command->contractId(), $command->contractType(),
$command->employmentPeriod(), $command->employerId(), $enteredContracts
)) {
throw new EmploymentPeriodOverlapsException();
}
}
}final class SignEmploymentContractHandler
{
public function __invoke(SignEmploymentContract $command): void
{
$contract = EmploymentContract::sign($command->contractId, $this->contractPeriodConfirmationService, /*...*/);
$this->contractCollection->save($contract);
}
}final class EmploymentContract extends AggregateRoot
{
public static function sign(ContractId $contractId, ContractPeriodConfirmationService $contractPeriodConfirmationService, /*...*/): EmploymentContract
{
try {
$contractPeriodConfirmationService->(/*...*/);
} catch (EmploymentPeriodOverlapsException $e) {
}
// ...
}
}Another solution would be to move the entire code to a separate Factory which would make the Application Service Command Handler very slim:
final class SignEmploymentContractHandler
{
public function __invoke(SignEmploymentContract $command): void
{
$contract = $this->employmentContractFactory->sign($command->contractId(), /*...*/);
$this->contractCollection->save($contract);
}
}Factories are recommended when creating an aggregate requires logic that does not naturally fit into the Aggregate Root or elsewhere.
Here are some very nice solutions suggested by @BrunoRommens:
Possibly related:
