You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All events of multiple aggregates have to be selected, changed in some domain relevant ways and be merged into a new aggregate.
I decided to select the events from the default repository using the metadata matcher.
I did not create a new aggregate by calling the factory methods with the new data. Since the data passed was already valid at the time to original events took place and were applied.
Instead I decided to use reflection to pass the "recorded" events.
At the bottom line a lot of prooph-inspired code was used. At the bottom line I think there is not too much coupling.
This example will have a unit test with an in-memory solution. Will add it soon.
Just like any other use case the new aggregate is stored to the repository. The event publisher publishes all events and the projector create the read models and process manager eventually publish some messages to the outside world.
Another process manager will catch the final MergedWithStaffMembers event and fire some "removeMergedStaffMember" commands.
Would love to have your feedback on this approach @Ocramius, @prolic.
The factory:
<?phpnamespaceAcme\Staff\Domain\Service;
useDomainException;
useProoph\EventSourcing\AggregateChanged;
useRamsey\Uuid\Uuid;
useReflectionClass;
useReflectionProperty;
useAcme\Staff\Domain\Model\StaffMember\ContractId;
useAcme\Staff\Domain\Model\StaffMember\EmploymentPeriod;
useAcme\Staff\Domain\Model\StaffMember\Event\MergedWithStaffMembers;
useAcme\Staff\Domain\Model\StaffMember\Event\StaffMemberAdded;
useAcme\Staff\Domain\Model\StaffMember\Event\StaffMemberContractModified;
useAcme\Staff\Domain\Model\StaffMember\StaffMember;
useAcme\Staff\Domain\Model\StaffMember\StaffMemberId;
useAcme\Staff\Domain\Model\StaffMember\StaffMemberRepository;
finalclass MergedStaffMember
{
/** @var StaffMemberRepository */private$staffMemberRepository;
/** * Current version * * @var int */private$version = 0;
/** * List of events that are not committed to the EventStore * * @var AggregateChanged[] */private$recordedEvents = [];
/** @var StaffMemberId */private$newStaffMemberId;
/** @var StaffMemberId[] */private$mergeWithStaffMemberIds = [];
/** @var ContractId */private$newContractId;
/** @var EmploymentPeriod */private$newEmploymentPeriod;
publicfunction__construct(StaffMemberRepository$staffMemberRepository)
{
$this->staffMemberRepository = $staffMemberRepository;
}
publicfunctionfromMergedHistory(
StaffMemberId$newStaffMemberId, array$mergeWithStaffMemberIds,
ContractId$newContractId, EmploymentPeriod$newEmploymentPeriod
): StaffMember
{
if (0 === count($mergeWithStaffMemberIds)) {
thrownewDomainException('Missing staff members to merge');
}
$this->newStaffMemberId = $newStaffMemberId;
$this->mergeWithStaffMemberIds = $mergeWithStaffMemberIds;
$this->newContractId = $newContractId;
$this->newEmploymentPeriod = $newEmploymentPeriod;
$this->buildHistoryFromMergedStaffMembers();
$this->finalizeNewStaffMemberHistory();
$newStaffMemberRef = newReflectionClass(StaffMember::class);
/** @var StaffMember $newStaffMember */$newStaffMember = $newStaffMemberRef->newInstanceWithoutConstructor();
$newStaffMemberRecordedEventsRef = newReflectionProperty($newStaffMember, 'recordedEvents');
$newStaffMemberRecordedEventsRef->setAccessible(true);
$newStaffMemberRecordedEventsRef->setValue($newStaffMember, $this->recordedEvents);
$newStaffMemberStaffMemberIdRef = newReflectionProperty($newStaffMember, 'staffMemberId');
$newStaffMemberStaffMemberIdRef->setAccessible(true);
$newStaffMemberStaffMemberIdRef->setValue($newStaffMember, $newStaffMemberId);
return$newStaffMember;
}
privatefunctionbuildHistoryFromMergedStaffMembers(): void
{
$oldEvents = $this->staffMemberRepository->ofStaffMemberIds($this->mergeWithStaffMemberIds);
// Ensure chronological orderuasort($oldEvents, function(AggregateChanged$a, AggregateChanged$b) {
return$a->createdAt() <=> $b->createdAt();
});
$initialStaffMemberId = StaffMemberId::fromString(reset($oldEvents)->aggregateId());
/** @var AggregateChanged[] $oldEvent */foreach ($oldEventsas$oldEvent) {
$newMessageData = $oldEvent->toArray();
// The new event needs an own unique ID.$newMessageData['uuid'] = Uuid::uuid4()->toString();
// Set the new staff member ID instead of the merged one.$newMessageData['metadata']['_aggregate_id'] = $this->newStaffMemberId->toString();
// This will be automatically reset correctly.
unset($newMessageData['metadata']['_position']);
if ($oldEventinstanceof StaffMemberAdded) {
/** @var StaffMemberAdded $oldEvent */if (!$oldEvent->staffMemberId()->sameValueAs($initialStaffMemberId)) {
// Only the initial event can add a staff member.// All other events can only be modifications of the contract.$newMessageData['message_name'] = StaffMemberContractModified::class;
}
}
if ($oldEventinstanceof StaffMemberAdded || $oldEventinstanceof StaffMemberContractModified) {
$newMessageData['payload']['contractId'] = $this->newContractId->toString();
// Set new employment period to satisfy all time-period relevant policies.$newMessageData['payload']['employmentPeriod'] = $this->newEmploymentPeriod->toArray();
}
$eventClassName = $newMessageData['message_name'];
$newEvent = $eventClassName::fromArray($newMessageData);
$this->recordThat($newEvent);
}
}
privatefunctionfinalizeNewStaffMemberHistory(): void
{
// Create final event$mergedWithStaffMembers = MergedWithStaffMembers::with(
$this->newStaffMemberId, $this->mergeWithStaffMemberIds,
$this->newContractId, $this->newEmploymentPeriod
);
$mergedWithStaffMembers = $mergedWithStaffMembers
->withAddedMetadata('_aggregate_type', StaffMember::class)
;
$this->recordThat($mergedWithStaffMembers);
}
/** * Record an aggregate changed event */protectedfunctionrecordThat(AggregateChanged$event): void
{
$this->version += 1;
$this->recordedEvents[] = $event->withVersion($this->version);
}
}
Actually MergedEmploymentContract is NOT an aggregate root. It's just a factory using the typical recordThat method of prooph which indeed is include in the AggregateRoot class. But that one is not extended here.
The factory is just not named "Factory" but after the result aggregate to expect. There is no "natural" aggregate that comes from the merging. It's just written to the regular Employment Contract stream.
On Mon, Aug 3, 2020, 15:33 webDEVILopers ***@***.***> wrote:
Actually MergedEmploymentContract is NOT an aggregate root. It's just a
factory using the typical recordThat method of prooph which indeed is
include in the AggregateRoot class. But that one is not extended here.
The factory is just not named "Factory" but after the result aggregate to
expect. There is no "natural" aggregate that comes from the merging. It's
just written to the regular Employment Contract stream.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#45 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AADAJPC54ATYNWF67O4NCBTR64GIPANCNFSM4PP4GXSQ>
.
All events of multiple aggregates have to be selected, changed in some domain relevant ways and be merged into a new aggregate.
I decided to select the events from the default repository using the metadata matcher.
I did not create a new aggregate by calling the factory methods with the new data. Since the data passed was already valid at the time to original events took place and were applied.
Instead I decided to use reflection to pass the "recorded" events.
This was inspired by @prooph code:
At the bottom line a lot of prooph-inspired code was used. At the bottom line I think there is not too much coupling.
This example will have a unit test with an in-memory solution. Will add it soon.
Just like any other use case the new aggregate is stored to the repository. The event publisher publishes all events and the projector create the read models and process manager eventually publish some messages to the outside world.
Another process manager will catch the final
MergedWithStaffMembers
event and fire some "removeMergedStaffMember" commands.Would love to have your feedback on this approach @Ocramius, @prolic.
The factory:
The command handler:
Just some framework - Symfony and YAML for Marco ;) - config:
The text was updated successfully, but these errors were encountered: