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

What happened?

What happened?

An introduction to CQRS and event sourcing in PHP

Avatar for Marijn Huizendveld

Marijn Huizendveld

August 21, 2014
Tweet

More Decks by Marijn Huizendveld

Other Decks in Programming

Transcript

  1. <?php ! final class Product { private $id; private $name;

    ! public function __construct($aName) { $this->name = (string) $aName; } ! public function getId() { return $this->id; } ! public function getName() { return $this->id; } }
  2. <?php ! class Customer { private $id; private $name; private

    $address; private $gender; ! public function getName() { return $this->name; } public function getAddress() { return $this->address; } public function getGender() { return $this->gender; } ! public function setName($name) { $this->name = $name; } public function setAddress($address) { $this->address = $address; } public function setGender($gender) { $this->gender = $gender; } }
  3. <?php ! /** * Publications are the contextual point for

    all other objects * * @author Marijn Huizendveld <[email protected]> * @version $Revision: 163 $ changed by $Author: buroknapzak $ * * @copyright De Baas Media (2010) */ class kluPublicationPeer extends BasekluPublicationPeer { /** * Select the publications that have recently been updated. * * @param string $arg_connection * * @return array */ static public function retrieveRecentlyUpdatedForDomain ($arg_domainUnifiedResourceName = NULL, $arg_connection = NULL) { $sql = "SELECT * FROM (SELECT `publications`.`id` AS ID " . ", `publications`.`name` AS NAME " . ", `publications`.`unified_resource_name` AS UNIFIED_RESOURCE_NAME " . ", `publications`.`domain_unified_resource_name` AS DOMAIN_UNIFIED_RESOURCE_NAME " . ", `publications`.`introduction` AS INTRODUCTION " . ", `publications`.`created_at` AS CREATED_AT " . ", MAX(`revision_requests`.`applied_at`) AS UPDATED_AT " . ", `publications`.`deleted_at` AS DELETED_AT " . "FROM `revision_requests` " . "LEFT JOIN `updates` " . "ON (`updates`.`id` = `revision_requests`.`update_id`) " . "LEFT JOIN `update_collections` " . "ON `updates`.`collection_unified_resource_name` = `update_collections`.`unified_resource_name` " . "LEFT JOIN `publications` " . "ON (`publications`.`id` = `revision_requests`.`publication_id`) " . "WHERE `updates`.`revision_request_id` IS NULL " . "AND `publications`.`deleted_at` IS NULL " . "AND `publications`.`domain_unified_resource_name` = ? " . "AND `update_collections`.`publish_at` <= NOW() " . "AND `update_collections`.`publish_until` > NOW() " . "GROUP BY `revision_requests`.`publication_id` " . "ORDER BY `updated_at` DESC) " . "`publications` ORDER BY `updated_at` DESC"; $connection = NULL === $arg_connection ? Propel::getConnection(self::DATABASE_NAME) : $arg_connection; $statement = $connection->prepareStatement($sql); return self::populateObjects($statement->executeQuery(array(0 => $arg_domainUnifiedResourceName), ResultSet::FETCHMODE_NUM)); } }
  4. No!

  5. <?php ! class Customer { private $id; private $name; private

    $address; private $gender; ! public function getName() { return $this->name; } public function getAddress() { return $this->address; } public function getGender() { return $this->gender; } ! public function setName($name) { $this->name = $name; } public function setAddress($address) { $this->address = $address; } public function setGender($gender) { $this->gender = $gender; } }
  6. <?php ! /** * Publications are the contextual point for

    all other objects * * @author Marijn Huizendveld <[email protected]> * @version $Revision: 163 $ changed by $Author: buroknapzak $ * * @copyright De Baas Media (2010) */ class kluPublicationPeer extends BasekluPublicationPeer { /** * Select the publications that have recently been updated. * * @param string $arg_connection * * @return array */ static public function retrieveRecentlyUpdatedForDomain ($arg_domainUnifiedResourceName = NULL, $arg_connection = NULL) { $sql = "SELECT * FROM (SELECT `publications`.`id` AS ID " . ", `publications`.`name` AS NAME " . ", `publications`.`unified_resource_name` AS UNIFIED_RESOURCE_NAME " . ", `publications`.`domain_unified_resource_name` AS DOMAIN_UNIFIED_RESOURCE_NAME " . ", `publications`.`introduction` AS INTRODUCTION " . ", `publications`.`created_at` AS CREATED_AT " . ", MAX(`revision_requests`.`applied_at`) AS UPDATED_AT " . ", `publications`.`deleted_at` AS DELETED_AT " . "FROM `revision_requests` " . "LEFT JOIN `updates` " . "ON (`updates`.`id` = `revision_requests`.`update_id`) " . "LEFT JOIN `update_collections` " . "ON `updates`.`collection_unified_resource_name` = `update_collections`.`unified_resource_name` " . "LEFT JOIN `publications` " . "ON (`publications`.`id` = `revision_requests`.`publication_id`) " . "WHERE `updates`.`revision_request_id` IS NULL " . "AND `publications`.`deleted_at` IS NULL " . "AND `publications`.`domain_unified_resource_name` = ? " . "AND `update_collections`.`publish_at` <= NOW() " . "AND `update_collections`.`publish_until` > NOW() " . "GROUP BY `revision_requests`.`publication_id` " . "ORDER BY `updated_at` DESC) " . "`publications` ORDER BY `updated_at` DESC"; $connection = NULL === $arg_connection ? Propel::getConnection(self::DATABASE_NAME) : $arg_connection; $statement = $connection->prepareStatement($sql); return self::populateObjects($statement->executeQuery(array(0 => $arg_domainUnifiedResourceName), ResultSet::FETCHMODE_NUM)); } }
  7. <?php ! class CustomerReadModel { private $id; private $name; private

    $address; private $gender; ! public function getName() { return $this->name; } public function getAddress() { return $this->address; } public function getGender() { return $this->gender; } }
  8. <?php ! class Customer { private $id; private $name; private

    $address; private $gender; ! public function setName($name) { $this->name = $name; } public function setAddress($address) { $this->address = $address; } public function setGender($gender) { $this->gender = $gender; } }
  9. <?php ! class Customer { private $id; private $name; private

    $address; private $gender; ! private function __construct($customerId, $name, $gender, Address $address) { $this->id = $customerId; $this->name = $name; $this->gender = $gender; $this->address = $address; } static public function signUp($customerId, $name, $gender, Address $address) { return new Customer($customerId, $name, $gender, $address); } ! public function moveToNewLivingAddress(Address $newAddress) { $this->address = $newAddress; } }
  10. <?php ! final class CustomerCommandHandler implements HandlesCommands { ! private

    $customers; ! public function __construct (CustomerRepository $aRepository) { $this->customers = $aRepository; } ! public function couldYou (Command $aCommand) { if ($aCommand instanceof PleaseSignCustomerUp) { $this->signUp($aCommand); } elseif ($aCommand instanceof PleaseMoveCustomerToNewLivingAddress) { $this->moveCustomerToNewLiving($aCommand); } else { // Intentionally empty: this will only happen during development. } } ! private function signUp (PleaseSignCustomerUp $aCommand) { $aCustomerId = $aCommand->customerId(); ! try { $aCustomer = $this->lockerServices->find($anEvent); // TODO: it exists, talk to the business how to deal with this } catch (SorryCustomerCouldNotBeFound $anException) { $aCustomer = Customer::signUp( $aCustomerId, $aCommand->name(), $aCommand->gender(), $aCommand->address() ); } ! $this->customers->add($aCustomer); } ! private function moveCustomerToNewLiving (PleaseMoveCustomerToNewLivingAddress $aCommand) { $aCustomerId = $aCommand->customerId(); $aCustomer = $this->customers->find($anEvent); ! $aCustomer->moveToNewLivingAddress($aCommand->address()); ! $this->lockerServices->add($aLockerService); } }
  11. <?php ! /** * Publications are the contextual point for

    all other objects * * @author Marijn Huizendveld <[email protected]> * @version $Revision: 163 $ changed by $Author: buroknapzak $ * * @copyright De Baas Media (2010) */ class kluPublicationPeer extends BasekluPublicationPeer { /** * Select the publications that have recently been updated. * * @param string $arg_connection * * @return array */ static public function retrieveRecentlyUpdatedForDomain ($arg_domainUnifiedResourceName = NULL, $arg_connection = NULL) { $sql = "SELECT * FROM (SELECT `publications`.`id` AS ID " . ", `publications`.`name` AS NAME " . ", `publications`.`unified_resource_name` AS UNIFIED_RESOURCE_NAME " . ", `publications`.`domain_unified_resource_name` AS DOMAIN_UNIFIED_RESOURCE_NAME " . ", `publications`.`introduction` AS INTRODUCTION " . ", `publications`.`created_at` AS CREATED_AT " . ", MAX(`revision_requests`.`applied_at`) AS UPDATED_AT " . ", `publications`.`deleted_at` AS DELETED_AT " . "FROM `revision_requests` " . "LEFT JOIN `updates` " . "ON (`updates`.`id` = `revision_requests`.`update_id`) " . "LEFT JOIN `update_collections` " . "ON `updates`.`collection_unified_resource_name` = `update_collections`.`unified_resource_name` " . "LEFT JOIN `publications` " . "ON (`publications`.`id` = `revision_requests`.`publication_id`) " . "WHERE `updates`.`revision_request_id` IS NULL " . "AND `publications`.`deleted_at` IS NULL " . "AND `publications`.`domain_unified_resource_name` = ? " . "AND `update_collections`.`publish_at` <= NOW() " . "AND `update_collections`.`publish_until` > NOW() " . "GROUP BY `revision_requests`.`publication_id` " . "ORDER BY `updated_at` DESC) " . "`publications` ORDER BY `updated_at` DESC"; $connection = NULL === $arg_connection ? Propel::getConnection(self::DATABASE_NAME) : $arg_connection; $statement = $connection->prepareStatement($sql); return self::populateObjects($statement->executeQuery(array(0 => $arg_domainUnifiedResourceName), ResultSet::FETCHMODE_NUM)); } }
  12. <?php ! /** * Publications are the contextual point for

    all other objects * * @author Marijn Huizendveld <[email protected]> * @version $Revision: 163 $ changed by $Author: buroknapzak $ * * @copyright De Baas Media (2010) */ class kluPublicationPeer extends BasekluPublicationPeer { /** * Select the publications that have recently been updated. * * @param string $arg_connection * * @return array */ static public function retrieveRecentlyUpdatedForDomain ($arg_domainUnifiedResourceName = NULL, $arg_connection = NULL) { $sql = "SELECT * FROM recently_updated WHERE domain = ? ORDER BY `updated_at` DESC"; $connection = NULL === $arg_connection ? Propel::getConnection(self::DATABASE_NAME) : $arg_connection; $statement = $connection->prepareStatement($sql); return self::populateObjects($statement->executeQuery(array(0 => $arg_domainUnifiedResourceName), ResultSet::FETCHMODE_NUM)); } }
  13. – Greg Young “(…) storing current state as a series

    of events and rebuilding state within the system by replaying that series of events.”
  14. <?php ! final class BasketWasPickedUp implements DomainEvent { private $basketId;

    ! public function __construct($basketId) { $this->basketId = (string) $basketId; } ! public function getAggregateId() { return $this->basketId; } }
  15. <?php ! final class Basket implements RecordsEvents { public static

    function pickUp(BasketId $basketId) { $basket = new BasketV2(); ! $basket->recordThat( new BasketWasPickedUp($basketId) ); ! return $basket; } protected function ackwnoledgesBasketWasPickedUp(BasketWasPickedUp $anEvent) { $this->basketId = $anEvent->basketId(); } ! private $basketId; private $latestRecordedEvents = []; private function __construct(BasketId $basketId) { // private constructor } ! private function recordThat(DomainEvent $domainEvent) { $this->latestRecordedEvents[] = $domainEvent; $this->ackwnoledge($domainEvent); } ! private function ackwnoledge(DomainEvent $event) { $method = 'ackwnoledge' . $event->name(); ! $this->$method($event); } ! public function getRecordedEvents() { return new DomainEvents($this->latestRecordedEvents); } ! public function clearRecordedEvents() { $this->latestRecordedEvents = []; } }
  16. <?php ! final class DebriefingOutboxEntryProjector { ! private $entries; !

    public function __construct (DebriefingOutboxEntryRepository $aRepository) { $this->entries = $aRepository; } ! /** * @param \Hank\Event $anEvent * * @throws \Entropt\LockerControl\DebriefingOutboxEntry\SorryDebriefingOutboxEntryCouldNotBePersisted */ public function project (DomainEvent $anEvent) { if ($anEvent instanceof DebriefCouldNotBeSent) { $this->debriefCouldNotBeSent($anEvent); } elseif ($anEvent instanceof DebriefWasSent) { $this->debriefWasSent($anEvent); } else { // intentionally empty: we don't want to break any long running processes } } ! private function debriefCouldNotBeSent (DebriefCouldNotBeSent $anEvent) { $this->entries->add(new DebriefingOutboxEntry(new Event($anEvent->event(), $anEvent->day()))); } ! private function debriefWasSent (DebriefWasSent $anEvent) { $aReference = new Event($anEvent->event(), $anEvent->day()); ! try { $anOnGoingEvent = $this->entries->find($aReference); ! $this->entries->remove($anOnGoingEvent); } catch (SorryDebriefingOutboxEntryCouldNotBeFound $anException) { // intentionally empty: we wanted to remove it, if it's not there than we just move on. } } }
  17. // Something happens ! $scenario->given($aPreCondition) // events ->andGiven($anotherPreCondition) ->when($aCommand) //

    commands ->then($anOutcome) // events ->andThen($anotherOutcome) ->butNothingElseShouldHaveHappened();
  18. <?php ! final class PleaseMakeLockersAvailableForRentTest extends CommandHandlerTestCase { ! /**

    * @test * @dataProvider ProvideNameOfEventDayOfEventNameOfLockerHostAndListOfAvailableLockers::that_are_not_nil */ public function makes_lockers_available_for_rent ( NameOfEvent $aName, DayOfEvent $aDay, NameOfLockerHost $aHost, UniqueLockerNumbers $aListOfAvailableLockers ) { $this->testScenarioThat ->when(PleaseMakeLockersAvailableForRent::v1($aName, $aDay, TimeOfCommand::rightNow(), $aHost, $aListOfAvailableLockers)) ! ->then(LockersWereMadeAvailableForRent::v1($aName, $aDay, TimeOfEvent::rightNow(), $aHost, $aListOfAvailableLockers)) ->butNothingElseShouldHaveHappened(); } ! /** * @test * @dataProvider ProvideNameOfEventDayOfEventNameOfLockerHostAndListOfAvailableLockers::that_are_not_nil */ public function ignores_command_when_an_event_with_identical_name_already_exists ( NameOfEvent $aName, DayOfEvent $aDay, NameOfLockerHost $aHost, UniqueLockerNumbers $aListOfAvailableLockers ) { $this->testScenarioThat ->given(LockersWereMadeAvailableForRent::v1($aName, $aDay, TimeOfEvent::fromString('Thursday March 6th 2014, 12:44:20 UTC'))) ! ->when(PleaseMakeLockersAvailableForRent::v1($aName, $aDay, TimeOfCommand::rightNow(), $aHost, $aListOfAvailableLockers)) ! ->thenNothingShouldHaveHappened(); } }