http://labs.qandidate.com
github.com/qandidate-labs/broadway
From that point in time
account_id | app_id | datetime |
---|---|---|
75623 | 8 | 2015-01-01T00:00:00+0000 |
75623 | 8 | 2015-01-27T18:54:18+0000 |
Past revocations are gone
The resulting state becomes a natural effect
There isn't one true answer
All 3 are optional, but work very nice together
The business should be reflected in your code
An model with an identity
Responsible for keeping a group of entities consistent
<?php // Company.php
public function __construct($id, $name)
{
$this->id = $id;
$this->name = $name;
}
<?php // Company.php
public static function register($id, $name)
{
$company = new Company();
$company->apply(
new CompanyRegisteredEvent($id, $name)
);
return $company;
}
<php // AggregateRoot.php
public function apply($event)
{
$this->handle($event);
$this->uncommitedEvents[] = $event;
}
private function handle($event)
{
$classParts = explode('\\', $event);
$method = 'apply' . end($classParts);
$this->$method($event);
}
Just one possible implementation
// Company.php
public function applyCompanyRegisteredEvent(
CompanyRegisteredEvent $event
) {
$this->companyId = $event->getCompanyId();
$this->companyName = $event->getCompanyName();
}
<php // CompanyRepository.php
function load($aggregateId)
{
$events = $this->eventStore->load($aggregateId);
$aggregate = new $this->aggregateClass();
$aggregate->initializeState($events);
return $aggregate;
}
<?php // Company.php
public function initializeState(array $events)
{
foreach ($events as $event)
{
$this->handle($event);
}
}
A message to tell your application what happened
It should only depend on previous events
final class CompanyRegisteredEvent
{
private $companyId;
private $companyName;
// constructor + getters
}
// Company
function applyCompanyRegisteredEvent(
CompanyRegisteredEvent $event
) {
$this->companyId = $event->getCompanyId();
}
// Company
function applyAccountConnectedEvent(
AccountConnectedEvent $event
) {
$id = $event->getAccountId();
$this->accounts[$id] = $event->getAccountId();
}
// Company
function applyAppEnabledEvent(
AppEnabledEvent $event
) {
$subscription = new Subscription(
$event->getCompanyId(),
$event->getAppId()
);
$this->subscriptions[$event->getAppId()] =
$subscription;
}
// Company
protected function getChildEntities()
{
return $this->subscriptions;
}
// Subscription
function applyAccessGrantedToAppEvent(
AccessGrantedToAppEvent $event
) {
if ($this->appId !== $event->getAppId()) {
return;
}
$accountId = $event->getAccountId();
$this->grantedAccounts[$accountId] =
$event->getAccountId();
}
Specific read models for specific views in your application
class CompanyRegistration implements ReadModel
{
public function __construct(
$companyId,
$companyName,
DateTime $registeredOn
) {
// ..
}
}
class CompanyRegistrationProjector
implements EventListener
{
public function applyCompanyRegisteredEvent(
CompanyRegisteredEvent $event,
DomainMessage $domainMessage
) {
$company = new CompanyRegistration(
$event->getCompanyId(),
$event->getCompanyName(),
$domainMessage->getRecordedOn()
);
$this->repository->save($company);
}
}
A CommandHandler deals with Commands and communicates with the Aggregate root
class CompanyController
{
function registerAction(Request $request)
{
$this->commandBus->dispatch(
new RegisterCompanyCommand(
Uuid::uuid4(),
$request->request->get('companyName')
)
);
}
}
class CompanyCommandHandler
{
function handleRegisterCompanyCommand(
RegisterCompanyCommand $command
) {
$company = Company::register(
$command->getCompanyId(),
$command->getCompanyName()
);
$this->aggregateRepostitory->save($company);
}
}
public static function register($companyId, $name)
{
$company = new Company();
$company->apply(
new CompanyRegisteredEvent($companyId, $name)
);
return $company;
}
public function applyCompanyRegisteredEvent(
CompanyRegisteredEvent $event
) {
$this->companyId = $event->getCompanyId();
}
// CompanyRegistry.php
function save($aggregate)
{
$events = $aggregate->getUncommittedEvents();
$this->eventStore->append($aggregate->getAggregateId(), $events);
$this->eventBus->publish($events);
}
class CompanyRegistrationProjector {
public function applyCompanyRegisteredEvent(
CompanyRegisteredEvent $event,
DomainMessage $domainMessage
) {
$company = new CompanyRegistration(
$event->getCompanyId(),
$event->getCompanyName(),
$domainMessage->getRecordedOn()
);
$this->repository->save($company);
}
}
Controller -> CommandBus -> CommandHandler -> AggregateRoot -> Event -> EventStore -> EventBus -> Projectors -> Read Model
Try not to reinvent everything yourself
Given - When - Then
$this->scenario
->given([
new CompanyRegisteredEvent(123)
])
->when(new EnableAppForCompanyCommand(42, 123))
->then([
new AppEnabledEvent(42, 123)
]);
$this->scenario
->given([
new CompanyRegisteredEvent(123)
])
->when(function ($company) {
$company->enableApp(42);
})
->then([
new AppEnabledEvent(42, 123)
]);
$this->scenario
->given([])
->when(
new CompanyRegisteredEvent(123, 'Acme Inc')
)
->then([
new CompanyRegistration(123, 'Acme Inc')
]);
Use events you recorded to create a new report
multiple years after the fact
So what? Correct your projector and recreate your read model from your event stream
@willemjanz
Freenode: #qandidate