Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Event Sourcing in practice @ Qandidate.com

Event Sourcing in practice @ Qandidate.com

A talk I gave first at Domcode Conference 2015.

Willem-Jan Zijderveld

November 14, 2015
Tweet

More Decks by Willem-Jan Zijderveld

Other Decks in Programming

Transcript

  1. Event sourcing in production
    Willem-Jan Zijderveld
    @willemjanz
    [email protected]
    joind.in/16227

    View Slide

  2. Event sourcing

    View Slide

  3. Short introduction
    into our domain

    View Slide

  4. Job Marketing Platform

    View Slide

  5. Choosing the
    best channels
    to find your candidates

    View Slide

  6. Online Job Marketing

    View Slide

  7. 1. Create a campaign for your target group

    View Slide

  8. 2. You get a recommendation

    View Slide

  9. 3. You can customize your campaign

    View Slide

  10. Why event sourcing?

    View Slide

  11. You are
    throwing away data!

    View Slide

  12. By only saving
    last known state

    View Slide

  13. You don't know the
    previous state

    View Slide

  14. You don't know why
    something changed

    View Slide

  15. How does event sourcing
    help?

    View Slide

  16. Using events to store
    changes

    View Slide

  17. Describe with your events
    what happened

    View Slide

  18. Describe with your events
    why something happened

    View Slide

  19. Use the events to
    calculate the current
    state

    View Slide

  20. Allows you
    to go back in time

    View Slide

  21. Helps to understand the
    flow in your application

    View Slide

  22. Example:
    Discarding the
    customizations

    View Slide

  23. CRUD

    View Slide

  24. Keep track if channel was
    part of recommendation

    View Slide

  25. Keep track if channel was
    part of recommendation
    Remove the non-recommended channels

    View Slide

  26. Keep track if channel was
    part of recommendation
    How about removed channels?

    View Slide

  27. Keep track if channel was
    part of recommendation
    Implement soft-delete

    View Slide

  28. Keep track if channel was
    part of recommendation
    Implement soft-delete
    for new campaigns

    View Slide

  29. Event sourcing

    View Slide

  30. Just rewind

    View Slide

  31. Just rewind
    Campaign
    Created For
    Target
    Group
    Channel
    Removed
    From
    Campaign
    Channel
    Added To
    Campaign
    Customization
    Discarded

    View Slide

  32. Just rewind
    Campaign
    Created For
    Target
    Group
    Channel
    Removed
    From
    Campaign
    Channel
    Added To
    Campaign
    Customization
    Discarded

    View Slide

  33. Just rewind
    Campaign
    Created For
    Target
    Group
    Channel
    Removed
    From
    Campaign
    Channel
    Added To
    Campaign
    Customization
    Discarded

    View Slide

  34. Just rewind
    Campaign
    Created For
    Target
    Group
    Channel
    Removed
    From
    Campaign
    Channel
    Added To
    Campaign
    Customization
    Discarded

    View Slide

  35. How?

    View Slide

  36. With the help of CQRS and
    Domain Driven Design

    View Slide

  37. And the help of Broadway
    github.com/qandidate-labs/broadway

    View Slide

  38. Creating a new campaign

    View Slide

  39. Campaign
    Event Store
    Create Campaign
    For Target Group
    Campaign Created For
    Target Group

    View Slide

  40. 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
    );
    }

    View Slide

  41. 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
    );
    }

    View Slide

  42. 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
    );
    }

    View Slide

  43. 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);
    }

    View Slide

  44. 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);
    }

    View Slide

  45. 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);
    }

    View Slide

  46. 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;
    }

    View Slide

  47. 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;
    }

    View Slide

  48. 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;
    }

    View Slide

  49. 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;
    }

    View Slide

  50. // Broadway: EventSourcedAggregateRoot.php
    public function apply($event)
    {
    $this->handleRecursively($event);
    $this->playhead++;
    $this->uncommittedEvents[] = DomainMessage::recordNow(
    $this->getAggregateRootId(),
    $this->playhead,
    new Metadata(),
    $event
    );
    }

    View Slide

  51. // Broadway: EventSourcedAggregateRoot.php
    public function apply($event)
    {
    $this->handleRecursively($event);
    $this->playhead++;
    $this->uncommittedEvents[] = DomainMessage::recordNow(
    $this->getAggregateRootId(),
    $this->playhead,
    new Metadata(),
    $event
    );
    }

    View Slide

  52. Campaign
    Event Store
    Create Campaign
    For Target Group
    Campaign Created For
    Target Group

    View Slide

  53. // 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
    );
    }

    View Slide

  54. // Broadway: EventSourcedAggregateRoot.php
    public function apply($event)
    {
    $this->handleRecursively($event);
    $this->playhead++;
    $this->uncommittedEvents[] = DomainMessage::recordNow(
    $this->getAggregateRootId(),
    $this->playhead,
    new Metadata(),
    $event
    );
    }

    View Slide

  55. // Broadway: EventSourcedAggregateRoot.php
    public function apply($event)
    {
    $this->handleRecursively($event);
    $this->playhead++;
    $this->uncommittedEvents[] = DomainMessage::recordNow(
    $this->getAggregateRootId(),
    $this->playhead,
    new Metadata(),
    $event
    );
    }

    View Slide

  56. // Broadway: EventSourcedAggregateRoot.php
    public function apply($event)
    {
    $this->handleRecursively($event);
    $this->playhead++;
    $this->uncommittedEvents[] = DomainMessage::recordNow(
    $this->getAggregateRootId(),
    $this->playhead,
    new Metadata(),
    $event
    );
    }

    View Slide

  57. 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);
    }

    View Slide

  58. 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);
    }

    View Slide

  59. 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);
    }

    View Slide

  60. 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);
    }

    View Slide

  61. It happened in the past

    View Slide

  62. Append only

    View Slide

  63. Campaign
    Event Store
    Create Campaign
    For Target Group
    Campaign Created For
    Target Group

    View Slide

  64. How would we load a
    campaign?

    View Slide

  65. Campaign
    Event Store
    Campaign Created For
    Target Group

    View Slide

  66. // 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());
    }
    }

    View Slide

  67. // 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());
    }
    }

    View Slide

  68. // 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());
    }
    }

    View Slide

  69. // 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());
    }
    }

    View Slide

  70. Let's add a channel

    View Slide

  71. Campaign
    Event Store
    Add Channel To
    Campaign
    Channel Added To
    Campaign

    View Slide

  72. // 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);
    }

    View Slide

  73. // 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);
    }

    View Slide

  74. // 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);
    }

    View Slide

  75. // 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);
    }

    View Slide

  76. // 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
    ));
    }

    View Slide

  77. // 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
    ));
    }

    View Slide

  78. // 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
    ));
    }

    View Slide

  79. // Campaign.php
    protected function applyChannelAddedToCampaign(
    ChannelAddedToCampaign $event
    ) {
    $this->channels[] = $event->getChannel();
    $this->campaignPrice = $event->getCampaignPrice();
    $this->estimatedOutcome = $event-
    >getEstimatedOutcomeOfCampaign();
    $this->isCustomized = true;
    }

    View Slide

  80. How do we get a listing
    of the campaigns?

    View Slide

  81. We'll create a read model

    View Slide

  82. Campaign
    Event Store
    Campaign Created For
    Target Group

    View Slide

  83. Campaign
    Event Store
    Campaign Created For
    Target Group
    Read Model
    Store

    View Slide

  84. Campaign
    Event Store
    Campaign Created For
    Target Group
    Read Model
    Store
    MySQL
    Read Model
    Store
    Redis

    View Slide

  85. 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);
    }

    View Slide

  86. 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()
    ));
    }

    View Slide

  87. 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()
    ));
    }

    View Slide

  88. 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()
    ));
    }

    View Slide

  89. 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);
    }

    View Slide

  90. 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);
    }

    View Slide

  91. 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);
    }

    View Slide

  92. Using the history to your
    advantage

    View Slide

  93. Discard Customizations

    View Slide

  94. // 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
    );
    }

    View Slide

  95. // 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']
    ));
    }

    View Slide

  96. // Campaign.php
    protected function applyCustomizationDiscarded(
    CustomizationDiscarded $event
    ) {
    $this->channels = $event->getRecommendedChannels();
    $this->campaignPrice = $event->getCampaignPrice();
    $this->estimatedOutcome = $event->getEstimatedOutcome();
    $this->isCustomized = false;
    }

    View Slide

  97. Where are the tests?

    View Slide

  98. Scenario testing

    View Slide

  99. Given
    When
    Then

    View Slide

  100. Campaign Created For
    Target Group
    Add Channel To
    Campaign
    Channel Added To
    Campaign

    View Slide

  101. $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 */)
    )
    ]);

    View Slide

  102. $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 */)
    )
    ]);

    View Slide

  103. $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 */)
    )
    ]);

    View Slide

  104. $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 */)
    )
    ]);

    View Slide

  105. 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 */)
    )
    ]);

    View Slide

  106. You can do something
    similar to test
    your read models

    View Slide

  107. Using the history to
    correct mistakes

    View Slide

  108. Replaying events to fix
    read models

    View Slide

  109. Replaying events to
    create new read models

    View Slide

  110. Very powerful

    View Slide

  111. Very tricky

    View Slide

  112. Replaying events
    Event Store
    Read Model
    Store
    e
    v
    e
    n
    t
    b
    u
    s Read Model
    Store

    View Slide

  113. Replaying events
    Event Store
    Read Model
    Store
    e
    v
    e
    n
    t
    b
    u
    s
    Email
    Notification
    Processor

    View Slide

  114. Find a solution for
    your specific problem

    View Slide

  115. Questions?
    joind.in/16227
    freenode: #qandidate
    github.com/qandidate-labs/broadway

    View Slide

  116. Symfony User Group
    The Renaissance
    10 december
    http://www.meetup.com/Symfony-User-Group-NL

    View Slide