Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Event Sourcing in practice @ Qandidate.com
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Willem-Jan Zijderveld
November 14, 2015
Programming
420
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Event Sourcing in practice @ Qandidate.com
A talk I gave first at Domcode Conference 2015.
Willem-Jan Zijderveld
November 14, 2015
More Decks by Willem-Jan Zijderveld
See All by Willem-Jan Zijderveld
Deploy je applicatie als een pakketje
wjzijderveld
0
120
Deploy your application in a box - DPC2016
wjzijderveld
0
250
Deploy your application in a box - 010PHP
wjzijderveld
0
250
Let's write some history - PHPDay 2016
wjzijderveld
1
310
Let's write some History - PHPBenelux 2016
wjzijderveld
1
340
Let's write some history - DPC 2015
wjzijderveld
1
320
Symfony CMF - A Decoupled Content Management Framework
wjzijderveld
0
150
PHPCR: A (better) way to structure content
wjzijderveld
0
210
Other Decks in Programming
See All in Programming
wkhtmltopdfの次どうするか問題2026
willnet
2
1.5k
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
470
Security issues being discussed on Web Platforms
petamoriken
0
980
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
270
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
350
AgentCore CLI で進化した AWS での AI エージェントの作り方 : 必要な機能を必要な時に
icoxfog417
PRO
3
350
モジュールの視点からSwiftを読み解く #iosdc
s_shimotori
0
180
setup-vp GitLab対応の裏側
naokihaba
0
110
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
500
AWS DevOps Agentで インシデント対応をAIに任せたい
honmarkhunt
7
3k
Are APIs Still Relevant in the AI Era?
soyuka
0
270
XP祭りでしか伝わらないフリップネタ #xpjug
murabayashi
0
150
Featured
See All Featured
4 Signs Your Business is Dying
shpigford
187
23k
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
410
Designing Experiences People Love
moore
143
24k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
234
18k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.6k
Understanding Cognitive Biases in Performance Measurement
bluesmoon
32
3k
Marketing to machines
jonoalderson
1
5.8k
A Tale of Four Properties
chriscoyier
163
24k
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
550
brightonSEO & MeasureFest 2025 - Christian Goodrich - Winning strategies for Black Friday CRO & PPC
cargoodrich
3
850
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Dominate Local Search Results - an insider guide to GBP, reviews, and Local SEO
greggifford
PRO
0
330
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