Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Event Sourcing in practice @ Qandidate.com
Search
Willem-Jan Zijderveld
November 14, 2015
Programming
1
360
Event Sourcing in practice @ Qandidate.com
A talk I gave first at Domcode Conference 2015.
Willem-Jan Zijderveld
November 14, 2015
Tweet
Share
More Decks by Willem-Jan Zijderveld
See All by Willem-Jan Zijderveld
Deploy je applicatie als een pakketje
wjzijderveld
0
83
Deploy your application in a box - DPC2016
wjzijderveld
0
210
Deploy your application in a box - 010PHP
wjzijderveld
0
200
Let's write some history - PHPDay 2016
wjzijderveld
1
240
Let's write some History - PHPBenelux 2016
wjzijderveld
1
270
Let's write some history - DPC 2015
wjzijderveld
1
250
Symfony CMF - A Decoupled Content Management Framework
wjzijderveld
0
100
PHPCR: A (better) way to structure content
wjzijderveld
0
160
Other Decks in Programming
See All in Programming
令和最新版手のひらコンピュータ
koba789
13
5.9k
React 使いじゃなくても知っておきたい教養としての React
oukayuka
18
5.4k
Terraform やるなら公式スタイルガイドを読もう 〜重要項目 10選〜
hiyanger
12
2.9k
バイブコーディングの正体——AIエージェントはソフトウェア開発を変えるか?
stakaya
5
790
なぜあなたのオブザーバビリティ導入は頓挫するのか
ryota_hnk
5
580
ZeroETLで始めるDynamoDBとS3の連携
afooooil
0
150
管你要 trace 什麼、bpftrace 用下去就對了 — COSCUP 2025
shunghsiyu
0
300
Infer入門
riru
4
1.1k
それ CLI フレームワークがなくてもできるよ / Building CLI Tools Without Frameworks
orgachem
PRO
17
3.7k
Google I/O Extended Incheon 2025 ~ What's new in Android development tools
pluu
1
230
Streamlitで実現できるようになったこと、実現してくれたこと
ayumu_yamaguchi
2
280
プロダクトという一杯を作る - プロダクトチームが味の責任を持つまでの煮込み奮闘記
hiliteeternal
0
420
Featured
See All Featured
The Invisible Side of Design
smashingmag
301
51k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
53
2.9k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
53k
How to train your dragon (web standard)
notwaldorf
96
6.2k
Statistics for Hackers
jakevdp
799
220k
How GitHub (no longer) Works
holman
314
140k
Navigating Team Friction
lara
188
15k
Music & Morning Musume
bryan
46
6.7k
Producing Creativity
orderedlist
PRO
347
40k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Scaling GitHub
holman
461
140k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
183
54k
Transcript
Event sourcing in production Willem-Jan Zijderveld @willemjanz
[email protected]
joind.in/16227
Event sourcing
Short introduction into our domain
Job Marketing Platform
Choosing the best channels to find your candidates
Online Job Marketing
1. Create a campaign for your target group
2. You get a recommendation
3. You can customize your campaign
Why event sourcing?
You are throwing away data!
By only saving last known state
You don't know the previous state
You don't know why something changed
How does event sourcing help?
Using events to store changes
Describe with your events what happened
Describe with your events why something happened
Use the events to calculate the current state
Allows you to go back in time
Helps to understand the flow in your application
Example: Discarding the customizations
CRUD
Keep track if channel was part of recommendation
Keep track if channel was part of recommendation Remove the
non-recommended channels
Keep track if channel was part of recommendation How about
removed channels?
Keep track if channel was part of recommendation Implement soft-delete
Keep track if channel was part of recommendation Implement soft-delete
for new campaigns
Event sourcing
Just rewind
Just rewind Campaign Created For Target Group Channel Removed From
Campaign Channel Added To Campaign Customization Discarded
Just rewind Campaign Created For Target Group Channel Removed From
Campaign Channel Added To Campaign Customization Discarded
Just rewind Campaign Created For Target Group Channel Removed From
Campaign Channel Added To Campaign Customization Discarded
Just rewind Campaign Created For Target Group Channel Removed From
Campaign Channel Added To Campaign Customization Discarded
How?
With the help of CQRS and Domain Driven Design
And the help of Broadway github.com/qandidate-labs/broadway
Creating a new campaign
Campaign Event Store Create Campaign For Target Group Campaign Created
For Target Group
Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto
$dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }
Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto
$dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }
Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto
$dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }
Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command
) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }
Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command
) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }
Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command
) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }
Action → Reaction // Campaign.php public static function createForTargetGroup(/* args
*/) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }
Action → Reaction // Campaign.php public static function createForTargetGroup(/* args
*/) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }
Action → Reaction // Campaign.php public static function createForTargetGroup(/* args
*/) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }
Action → Reaction // Campaign.php public static function createForTargetGroup(/* args
*/) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }
// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[]
= DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }
// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[]
= DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }
Campaign Event Store Create Campaign For Target Group Campaign Created
For Target Group
// Campaign.php protected function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $this->id
= $event->getCampaignId(); $this->channels = $event->getRecommendedChannels()->toArray(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->targetGroup = $event->getTargetGroup(); $this->setInitialRecommendation( $this->channels, $this->estimatedOutcome, $this->campaignPrice ); }
// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[]
= DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }
// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[]
= DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }
// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[]
= DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }
Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command
) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender, $this->portfolio, $this->aidaMetricsCalculator ); $this->campaignRepository->save($campaign); }
Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate)
{ // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }
Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate)
{ // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }
Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate)
{ // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }
It happened in the past
Append only
Campaign Event Store Create Campaign For Target Group Campaign Created
For Target Group
How would we load a campaign?
Campaign Event Store Campaign Created For Target Group
// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream
= $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }
// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream
= $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }
// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream
= $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }
// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream
= $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }
Let's add a channel
Campaign Event Store Add Channel To Campaign Channel Added To
Campaign
// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel
= $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }
// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel
= $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }
// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel
= $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }
// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel
= $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }
// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator )
{ $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }
// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator )
{ $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }
// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator )
{ $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }
// Campaign.php protected function applyChannelAddedToCampaign( ChannelAddedToCampaign $event ) { $this->channels[]
= $event->getChannel(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event- >getEstimatedOutcomeOfCampaign(); $this->isCustomized = true; }
How do we get a listing of the campaigns?
We'll create a read model
Campaign Event Store Campaign Created For Target Group
Campaign Event Store Campaign Created For Target Group Read Model
Store
Campaign Event Store Campaign Created For Target Group Read Model
Store MySQL Read Model Store Redis
Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate)
{ // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }
Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event,
DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }
Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event,
DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }
Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event,
DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }
Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event
) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }
Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event
) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }
Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event
) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }
Using the history to your advantage
Discard Customizations
// Campaign.php protected function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $this->id
= $event->getCampaignId(); $this->channels = $event->getRecommendedChannels()->toArray(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->targetGroup = $event->getTargetGroup(); $this->setInitialRecommendation( $this->channels, $this->estimatedOutcome, $this->campaignPrice ); }
// Campaign.php public function discardCustomization() { $this->guardCannotModifyOrderedCampaign(); if ($this->isCustomized ===
false) { return; } $this->apply(new CustomizationDiscarded( $this->id, $this->initialRecommendation['channels'], $this->initialRecommendation['campaignPrice'], $this->initialRecommendation['estimatedOutcome'] )); }
// Campaign.php protected function applyCustomizationDiscarded( CustomizationDiscarded $event ) { $this->channels
= $event->getRecommendedChannels(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->isCustomized = false; }
Where are the tests?
Scenario testing
Given When Then
Campaign Created For Target Group Add Channel To Campaign Channel
Added To Campaign
$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, //
$args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);
$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, //
$args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);
$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, //
$args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);
$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, //
$args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);
Alternative scenario test $channelId = new ChannelId('42'); $this->scenario ->given([ new
CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(function (Campaign $campaign) { $campaign->addChannel($channelId) }) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);
You can do something similar to test your read models
Using the history to correct mistakes
Replaying events to fix read models
Replaying events to create new read models
Very powerful
Very tricky
Replaying events Event Store Read Model Store e v e
n t b u s Read Model Store
Replaying events Event Store Read Model Store e v e
n t b u s Email Notification Processor
Find a solution for your specific problem
Questions? joind.in/16227 freenode: #qandidate github.com/qandidate-labs/broadway
Symfony User Group The Renaissance 10 december http://www.meetup.com/Symfony-User-Group-NL