-
-
Save phcostabh/17bcff40b945808a38e8d14542c69854 to your computer and use it in GitHub Desktop.
Revisions
-
Philippe Santana Costa revised this gist
Dec 18, 2018 . 1 changed file with 31 additions and 17 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -274,7 +274,8 @@ The applyLessonWasOpened and applyClientWasBookedOntoLesson methods might seem a It's a tricky concept to explain so here is some code to help outline the process. Later, we will extract out the code handling uncommittedEvents and domain event message creation. <div class="code-snippit-filename">app/School/Lesson/Lesson.php</div> ```php <?php @@ -343,7 +344,8 @@ We can extract the CQRS components out from our write model - the parts of the The extracted class is an implementation detail of CQRS, and doesn't contain any domain logic so we can create a new namespace: App\CQRS: <div class="code-snippit-filename">app/CQRS/EventSourcedEntity.php</div> ```php <?php namespace App\CQRS; @@ -386,7 +388,8 @@ abstract class EventSourcedEntity implements EventSourcedEntityInterface { To get the code to run, we need to add a DomainEventMessage class, which is just a simple DTO: <div class="code-snippit-filename">app/CQRS/DomainEventMessage.php</div> ```php <?php namespace App\CQRS; @@ -473,12 +476,14 @@ The Serializer will be responsible for recording the event class, whilst the eve event: $event->serialize() } ``` Since all events will need a serialize and deserialize method, we can create a SerializableEvent interface, and start type hinting :) Update our LessonWasOpened event: <div class="code-snippit-filename">app/School/Lesson/Events/LessonWasOpened.php</div> ```php class LessonWasOpened implements SerializableEvent { public function serialize() @@ -491,7 +496,8 @@ class LessonWasOpened implements SerializableEvent { Create the LessonRepository. We can refactor and extract generic CQRS stuff later. <div class="code-snippit-filename">app/School/Lesson/LessonRepository.php</div> ```php <?php namespace App\School\Lesson; @@ -538,7 +544,8 @@ Our final step to get our test to pass: listening for the broadcasted events and The broadcasted Lesson events will be caught by a LessonProjector, which applies the neccessary changes to a LessonProjection (just an Eloquent model of the lessons table): <div class="code-snippit-filename">app/School/Lesson/Projections/LessonProjector.php</div> ```php <?php namespace App\School\Lesson\Projections; @@ -592,7 +599,8 @@ And obviously, don't forget to register the event subscriber with laravel: : <div class="code-snippit-filename">app/Providers/EventServiceProvider.php</div> ```php public function boot(DispatcherContract $events) { parent::boot($events); @@ -657,7 +665,8 @@ Our write model currently has no way of loading the state of existing. If we wan To explain what I mean, let's write a second test simulating booking two clients into a lesson. <div class="code-snippit-filename">tests/CQRSTest.php</div> ```php public function testLoadingWriteModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440001'; @@ -698,7 +707,8 @@ The basic process is: At the moment, our tests throws exceptions so we start by creating the required BookClientOntoLesson command, using the BookLesson command as a template. The handle method will look like: <div class="code-snippit-filename">app/School/Lesson/Commands/BookClientOntoLesson.php</div> ```php public function handle(LessonRepository $repository) { /** @var Lesson $lesson */ $lesson = $repository->load($this->lessonId); @@ -711,7 +721,7 @@ Add the load event in the lesson repository: <div class="code-snippit-filename">app/School/Lesson/LessonRepository.php</div> ```php public function load(LessonId $id) { $events = $this->eventStoreRepository->load($id); $lesson = new Lesson(); @@ -729,7 +739,7 @@ Let's take a look at the code to explain things better. First the EventStoreRepository's load function: <div class="code-snippit-filename">app/CQRS/EloquentEventStoreRepository.php</div> ```php public function load($uuid) { $eventMessages = EloquentEventStoreModel::where('uuid', $uuid)->get(); $events = []; @@ -749,7 +759,8 @@ public function load($uuid) { With the corresponding deserialize function in the eventSerializer: <div class="code-snippit-filename">app/CQRS/Serializer/EventSerializer.php</div> ```php public function deserialize( $serializedEvent ) { $eventClass = $serializedEvent->class; $eventPayload = $serializedEvent->payload; @@ -762,7 +773,7 @@ Finally, the static factory deserialize() method in LessonWasOpened (we need to <div class="code-snippit-filename">app/School/Lesson/Events/LessonWasOpened.php</div> ```php public static function deserialize($data) { $lessonId = new LessonId($data->lessonId); return new self($lessonId); @@ -773,7 +784,7 @@ Now we have an array of all previous events, we just replay them against our Ent <div class="code-snippit-filename">app/CQRS/EventSourcedEntity.php</div> ```php public function initializeState($events) { foreach( $events as $event ) { $this->handle($event); @@ -793,7 +804,8 @@ And rerun our test - we're back to Green! We don't actually have a test here to ensure we're enforcing our domain rules, so let's write one: <div class="code-snippit-filename">tests/CQRSTest.php</div> ```php public function testMoreThan3ClientsCannotbeAddedToALesson() { $testLessonId = '123e4567-e89b-12d3-a456-426655440002'; $lessonId = new LessonId($testLessonId); @@ -823,7 +835,8 @@ At the moment, we're just passing in hand crafted UUIDs when really we want to g And update our tests to use the new package: <div class="code-snippit-filename">tests/CQRSTest.php</div> ```php public function testEntityCreationWithUUIDGenerator() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $this->dispatch( new BookLesson($lessonId, "bob") ); @@ -841,7 +854,8 @@ public function testEntityCreationWithUUIDGenerator() { Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel class which extends the Eloquent Model class and overrides the save method: <div class="code-snippit-filename">app/CQRS/ReadModelImmutableModel.php</div> ```php <?php namespace App\CQRS\ReadModel; -
Philippe Santana Costa revised this gist
Dec 18, 2018 . 1 changed file with 15 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -85,7 +85,8 @@ Start a new laravel 5 project And the first thing we need is a test. We're just going to stick with an integration test to assert that booking a lesson for a client results in that lesson being created in our Eloquent model. <div class="code-snippit-filename">tests/CQRSTest.php</div> ```php <?php use Illuminate\Foundation\Bus\DispatchesCommands; @@ -98,7 +99,7 @@ class CQRSTest extends TestCase { * Assert the BookLesson Command creates a lesson in our read projection * @return void */ public function testFiringEventUpdatesReadModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440000'; $clientName = "George"; @@ -111,13 +112,14 @@ class CQRSTest extends TestCase { $this->assertEquals( Lesson::find($testLessonId)->clientName, $clientName ); } } ``` We provide the ID for our new lesson up front, create a command to book a new lesson and tell Laravel to dispatch the command. We want a new entry to be created in the lessons table, which we can read with an Eloquent model. We will be needing a database, so fill in your .env file as required. Each event logged to our event store is attached to an Aggregate Root (which we will just call an Entity - the abstraction for learning purposes just adds to the confusion). This ID is a universally unique ID. The event store doesn't care if the event is to be applied to a Lesson or a Client, it just knows it's attached to an ID. <a name="missing-classes"></a> ### Follow the test ### <blockquote><code class="hljs">Following along? @@ -129,11 +131,13 @@ Being driven by our test errors, we can create our missing classes. First we cre I'm using an assertion library to keep the code clean. You can grab it with: <blockquote><code class="hljs"> $> composer require beberlei/assert </code></blockquote> <div class="code-snippit-filename">app/School/Lesson/LessonId.php</div> ```php <?php namespace App\School\Lesson; @@ -161,7 +165,8 @@ class LessonId { ``` <div class="code-snippit-filename">app/School/Lesson/Commands/BookLesson.php</div> ```php <?php namespace App\School\Lesson\Commands; @@ -195,7 +200,8 @@ class BookLesson extends Command implements SelfHandling { ``` <div class="code-snippit-filename">app/School/ReadModels/Lesson.php</div> ```php <?php namespace App\School\ReadModels; @@ -207,7 +213,8 @@ class Lesson extends Model { ``` <div class="code-snippit-filename">database/migrations/createlessontable.php</div> ```php <?php use Illuminate\Database\Schema\Blueprint; -
scazz revised this gist
May 12, 2015 . 1 changed file with 2 additions and 7 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -79,8 +79,7 @@ Event Sourcing also gives you the ability to process the events off-line. This p Start a new laravel 5 project <blockquote><code class="hljs">$> laravel new cqrs-tutorial </code></blockquote> And the first thing we need is a test. We're just going to stick with an integration test to assert that booking a lesson for a client results in that lesson being created in our Eloquent model. @@ -130,11 +129,7 @@ Being driven by our test errors, we can create our missing classes. First we cre I'm using an assertion library to keep the code clean. You can grab it with: <blockquote><code class="hljs">$> composer require beberlei/assert </code></blockquote> <div class="code-snippit-filename">app/School/Lesson/LessonId.php</div> -
scazz revised this gist
May 12, 2015 . 1 changed file with 55 additions and 38 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -79,7 +79,7 @@ Event Sourcing also gives you the ability to process the events off-line. This p Start a new laravel 5 project <blockquote><code class="hljs"> $> laravel new cqrs-tutorial </code></blockquote> @@ -133,6 +133,9 @@ I'm using an assertion library to keep the code clean. You can grab it with: ``` $> composer require beberlei/assert ``` <blockquote><code class="hljs">Following along? $> git checkout c60aa7b </code></blockquote> <div class="code-snippit-filename">app/School/Lesson/LessonId.php</div> ``` @@ -162,6 +165,7 @@ class LessonId { } ``` <div class="code-snippit-filename">app/School/Lesson/Commands/BookLesson.php</div> ``` <?php namespace App\School\Lesson\Commands; @@ -195,6 +199,7 @@ class BookLesson extends Command implements SelfHandling { } ``` <div class="code-snippit-filename">app/School/ReadModels/Lesson.php</div> ``` <?php namespace App\School\ReadModels; @@ -206,6 +211,7 @@ class Lesson extends Model { } ``` <div class="code-snippit-filename">database/migrations/createlessontable.php</div> ``` <?php @@ -265,6 +271,7 @@ The applyLessonWasOpened and applyClientWasBookedOntoLesson methods might seem a It's a tricky concept to explain so here is some code to help outline the process. Later, we will extract out the code handling uncommittedEvents and domain event message creation. <div class="code-snippit-filename">app/School/Lesson/Lesson.php</div> ``` <?php @@ -324,15 +331,16 @@ class Lesson { <a name="extract-refactor-write-model"></a> ### Extracting CQRS components and refactoring the write model ### <blockquote><code class="hljs">Following along? $> git checkout 9c148fe </code></blockquote> We can extract the CQRS components out from our write model - the parts of the class dealing with uncommitted events. We can also clean up the API for an event sourced entity by creating a protected function called apply() which takes an event, calls the appropriate applyEventName() method and appends a new DomainEventMessage to the uncommitted events list. The extracted class is an implementation detail of CQRS, and doesn't contain any domain logic so we can create a new namespace: App\CQRS: <div class="code-snippit-filename">app/CQRS/EventSourcedEntity.php</div> ``` <?php namespace App\CQRS; @@ -375,9 +383,10 @@ abstract class EventSourcedEntity implements EventSourcedEntityInterface { To get the code to run, we need to add a DomainEventMessage class, which is just a simple DTO: <div class="code-snippit-filename">app/CQRS/DomainEventMessage.php</div> ``` <?php namespace App\CQRS; use DateTime; @@ -429,10 +438,9 @@ class DomainEventMessage { <a name="persisting-events"></a> ### Persisting events to an event store ### <blockquote><code class="hljs">Following along? $> git checkout 92ee32e </code></blockquote> So, we have a system which generates events for every write and uses the events to recorded changes necessary to prevent invariants. The next step is to persist these events to the EventStore. @@ -463,11 +471,11 @@ The Serializer will be responsible for recording the event class, whilst the eve event: $event->serialize() } ``` Since all events will need a serialize and deserialize method, we can create a SerializableEvent interface, and start type hinting :) Update our LessonWasOpened event: <div class="code-snippit-filename">app/School/Lesson/Events/LessonWasOpened.php</div> ``` class LessonWasOpened implements SerializableEvent { @@ -480,6 +488,7 @@ class LessonWasOpened implements SerializableEvent { Create the LessonRepository. We can refactor and extract generic CQRS stuff later. <div class="code-snippit-filename">app/School/Lesson/LessonRepository.php</div> ``` <?php namespace App\School\Lesson; @@ -517,15 +526,16 @@ If you run the integration test again, then check the domain_events SQL table, y <a name="updating-read-model"></a> ### Listing to events and updating read models ### <blockquote><code class="hljs">Following along? $> git checkout e202739 </code></blockquote> Our final step to get our test to pass: listening for the broadcasted events and updating the Lesson read model projection. The broadcasted Lesson events will be caught by a LessonProjector, which applies the neccessary changes to a LessonProjection (just an Eloquent model of the lessons table): <div class="code-snippit-filename">app/School/Lesson/Projections/LessonProjector.php</div> ``` <?php namespace App\School\Lesson\Projections; @@ -563,6 +573,7 @@ class LessonProjector { } ``` <div class="code-snippit-filename">app/School/Lesson/Projections/LessonProjection.php</div> ``` <?php namespace App\School\Lesson\Projections; @@ -576,8 +587,9 @@ class LessonProjection extends Model { ``` And obviously, don't forget to register the event subscriber with laravel: : <div class="code-snippit-filename">app/Providers/EventServiceProvider.php</div> ``` public function boot(DispatcherContract $events) { @@ -587,27 +599,27 @@ App\Providers\EventServiceProvider.php: ``` If you run the test, you'll see that there is an SQL error: <blockquote><code class="hljs">Unknown column 'clientName' in 'field list' </code></blockquote> Once we create a migration to add clientName to the lessons table, our test should pass :). We have implemented basic CQRS functionality: commands create events which are used to generate read models. <a name="read-model-relationships"></a> ### Improving the read model with relationships ### <blockquote><code class="hljs">Following along? $> git checkout 3a70e75e </code></blockquote> We've reach a mile stone, but we're not there yet! The read model only has support for one client (we specified 3 in our domain rules). The changes we make to the read model are fairly straight forward; we just create a Client projection model and ClientProjector which catches the ClientBookedOntoLesson event. First, let's update our test to reflect the changes we want to see in our read model: <div class="code-snippit-filename">tests/CQRSTest.php</div> ``` public function testFiringEventUpdatesReadModel() { @@ -634,15 +646,15 @@ Smoke testing is something we get for free with an event sourced system - if we <a name="loading-lessons"></a> ### Loading lessons ### <blockquote><code class="hljs">Following along? $> git checkout a10888e </code></blockquote> Our write model currently has no way of loading the state of existing. If we want to add a second client to a lesson, we could just create a second ClientWasAddedToLesson event but we wouldn't be able to protect against invariants. To explain what I mean, let's write a second test simulating booking two clients into a lesson. <div class="code-snippit-filename">tests/CQRSTest.php</div> ``` public function testLoadingWriteModel() { @@ -683,6 +695,7 @@ The basic process is: At the moment, our tests throws exceptions so we start by creating the required BookClientOntoLesson command, using the BookLesson command as a template. The handle method will look like: <div class="code-snippit-filename">app/School/Lesson/Commands/BookClientOntoLesson.php</div> ``` public function handle(LessonRepository $repository) { /** @var Lesson $lesson */ @@ -694,6 +707,8 @@ public function handle(LessonRepository $repository) { Add the load event in the lesson repository: <div class="code-snippit-filename">app/School/Lesson/LessonRepository.php</div> ``` public function load(LessonId $id) { $events = $this->eventStoreRepository->load($id); @@ -709,7 +724,8 @@ The Serializer created the messages from events, so we need to add a deserialize Let's take a look at the code to explain things better. First the EventStoreRepository's load function: <div class="code-snippit-filename">app/CQRS/EloquentEventStoreRepository.php</div> ``` public function load($uuid) { @@ -730,6 +746,7 @@ public function load($uuid) { With the corresponding deserialize function in the eventSerializer: <div class="code-snippit-filename">app/CQRS/Serializer/EventSerializer.php</div> ``` public function deserialize( $serializedEvent ) { $eventClass = $serializedEvent->class; @@ -741,6 +758,8 @@ public function deserialize( $serializedEvent ) { Finally, the static factory deserialize() method in LessonWasOpened (we need to add this method to every event) <div class="code-snippit-filename">app/School/Lesson/Events/LessonWasOpened.php</div> ``` public static function deserialize($data) { $lessonId = new LessonId($data->lessonId); @@ -750,7 +769,8 @@ public static function deserialize($data) { Now we have an array of all previous events, we just replay them against our Entity write model to initialize state: <div class="code-snippit-filename">app/CQRS/EventSourcedEntity.php</div> ``` public function initializeState($events) { foreach( $events as $event ) { @@ -764,13 +784,13 @@ And rerun our test - we're back to Green! <a name="invariants"></a> ### Protecting against invariants ### <blockquote><code class="hljs">Following along? $> git checkout eabe2cf </code></blockquote> We don't actually have a test here to ensure we're enforcing our domain rules, so let's write one: <div class="code-snippit-filename">tests/CQRSTest.php</div> ``` public function testMoreThan3ClientsCannotbeAddedToALesson() { $testLessonId = '123e4567-e89b-12d3-a456-426655440002'; @@ -789,19 +809,18 @@ You'll notice that we only need a lessonId - this test re-initializing the lesso <a name="uuids"></a> ### Generating UUIDs ### <blockquote><code class="hljs">Following along? $> git checkout eeeac9e </code></blockquote> At the moment, we're just passing in hand crafted UUIDs when really we want to generate these automatically. I'm going to use the Ramsy\UUID package, so let's install that with composer: <blockquote><code class="hljs">$> composer require ramsey/uuid </code></blockquote> And update our tests to use the new package: <div class="code-snippit-filename">tests/CQRSTest.php</div> ``` public function testEntityCreationWithUUIDGenerator() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); @@ -813,15 +832,13 @@ public function testEntityCreationWithUUIDGenerator() { <a name="immutable-models"></a> ### "Enforcing" Read Model immutability ### <blockquote><code class="hljs">Following along? $> git checkout 5e62320 </code></blockquote> Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel class which extends the Eloquent Model class and overrides the save method: <div class="code-snippit-filename">app/CQRS/ReadModelImmutableModel.php</div> ``` <?php namespace App\CQRS\ReadModel; -
scazz revised this gist
May 12, 2015 . 1 changed file with 8 additions and 9 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -73,15 +73,15 @@ Event Sourcing also gives you the ability to process the events off-line. This p <a name="project-setup"></a> ### Project Set up and first test ### <blockquote><code class="hljs">Following along? $> git checkout c60aa7b </code></blockquote> Start a new laravel 5 project <blockquote><code class="hljs">Following along? $> laravel new cqrs-tutorial </code></blockquote> And the first thing we need is a test. We're just going to stick with an integration test to assert that booking a lesson for a client results in that lesson being created in our Eloquent model. @@ -121,10 +121,10 @@ Each event logged to our event store is attached to an Aggregate Root (which we <a name="missing-classes"></a> ### Follow the test ### <blockquote><code class="hljs">Following along? $> git checkout 284da0f9 </code></blockquote> Being driven by our test errors, we can create our missing classes. First we create a LessonId class, followed by the BookLesson command (don't worry about the handle method yet, just keep following the test). The Lesson class is a read model outside of the Lesson namespace - it will only ever be a read model - no domain logic will reside in here. Finally, we have to create a migration for the lessons table. @@ -243,10 +243,9 @@ class CreateLessonTable extends Migration { <a name="pseudo-lesson-write-model"></a> ### Booking a lesson using events - an overview ### <blockquote><code class="hljs">Following along? $> git checkout bd4e01a4 </code></blockquote> Let's take a look at the process this command should initiate: -
scazz revised this gist
May 12, 2015 . 1 changed file with 38 additions and 14 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,18 +1,40 @@ <style> blockquote { border: 1px solid #dddddd; border-radius: 5px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 1.36363636em; padding: 0; } blockquote > code { background-color: #ffffff !important; border-left: 3em solid #f0f0f0; font-size: 13px; padding-left: 1.5em !important; } .code-snippit-filename { background: none; border: 1px solid #dddddd; border-top-left-radius: 5px; border-top-right-radius: 5px; margin-bottom: 0px; padding: 5px; border-bottom: 0px; background-color: #f5f5f5; text-align: left; padding-left: 55px; font-size: 15px; height: 40px; line-height: 30px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); } .code-snippit-filename + pre { border-top-left-radius: 0px; border-top-right-radius: 0px } </style> ### Contents ### @@ -51,7 +73,7 @@ Event Sourcing also gives you the ability to process the events off-line. This p <a name="project-setup"></a> ### Project Set up and first test ### <blockquote><code>Following along? $> git checkout c60aa7b </code></blockquote> @@ -63,6 +85,7 @@ Start a new laravel 5 project And the first thing we need is a test. We're just going to stick with an integration test to assert that booking a lesson for a client results in that lesson being created in our Eloquent model. <div class="code-snippit-filename">tests/CQRSTest.php</div> ``` <?php @@ -111,6 +134,7 @@ I'm using an assertion library to keep the code clean. You can grab it with: $> composer require beberlei/assert ``` <div class="code-snippit-filename">app/School/Lesson/LessonId.php</div> ``` <?php namespace App\School\Lesson; -
scazz revised this gist
May 12, 2015 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -4,6 +4,7 @@ blockquote { border-radius: 5px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 1.36363636em; padding: 0; } blockquote > code { -
scazz revised this gist
May 12, 2015 . 1 changed file with 5 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,16 +1,16 @@ <style> blockquote { border: 1px solid #dddddd; border-radius: 5px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 1.36363636em; } blockquote > code { background-color: #ffffff !important; border-left: 3em solid #f0f0f0; font-size: 13px; padding-left: 1.5em !important; } </style> @@ -50,8 +50,7 @@ Event Sourcing also gives you the ability to process the events off-line. This p <a name="project-setup"></a> ### Project Set up and first test ### <blockquote><code class="hljs no-border">Following along? $> git checkout c60aa7b </code></blockquote> -
scazz revised this gist
May 12, 2015 . 1 changed file with 18 additions and 6 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,19 @@ <style> blockquotes { border: 1px solid #dddddd; border-radius: 5px; box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 1.36363636em; } blockquotes > code { background-color: #ffffff; border-left: 3em solid #f0f0f0; font-size: 13px; padding-left: 1.5em; } </style> ### Contents ### * [Introduction](#introduction) * [Project Setup and first test](#project-setup) @@ -34,14 +50,10 @@ Event Sourcing also gives you the ability to process the events off-line. This p <a name="project-setup"></a> ### Project Set up and first test ### <blockquote><code class="hljs no-border"> Following along? $> git checkout c60aa7b </code></blockquote> Start a new laravel 5 project -
scazz revised this gist
May 12, 2015 . 1 changed file with 8 additions and 4 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -34,10 +34,14 @@ Event Sourcing also gives you the ability to process the events off-line. This p <a name="project-setup"></a> ### Project Set up and first test ### <blockquote> <code class="hljs no-border"> Following along? $> git checkout c60aa7b </code> </blockquote> Start a new laravel 5 project @@ -774,12 +778,12 @@ public function testEntityCreationWithUUIDGenerator() { <a name="immutable-models"></a> ### "Enforcing" Read Model immutability ### <span class="no-line-numbers"></span> ~~~ Following along? $> git checkout 5e62320 ~~~ Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel class which extends the Eloquent Model class and overrides the save method: -
scazz revised this gist
May 12, 2015 . 1 changed file with 3 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -774,10 +774,12 @@ public function testEntityCreationWithUUIDGenerator() { <a name="immutable-models"></a> ### "Enforcing" Read Model immutability ### <div class="no-line-numbers"> ~~~ Following along? $> git checkout 5e62320 ~~~ </div> Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel class which extends the Eloquent Model class and overrides the save method: @@ -798,4 +800,4 @@ class ImmutableModel extends Model { } ``` Obviously a determined developer can still update the read model if they want to, but this will help prevent confusion. -
scazz revised this gist
May 12, 2015 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -8,7 +8,7 @@ * [Listing to events and updating read models](#updating-read-model) * [Improving the read model with relationships](#read-model-relationships) * [Loading lessons](#loading-lessons) * [Protecting against invariants](#invariants) * [Generating UUIDs](#uuids) * ["Enforcing" Read Model immutability](#immutable-models) -
scazz revised this gist
May 12, 2015 . 1 changed file with 183 additions and 49 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -4,12 +4,18 @@ * [Follow the test](#missing-classes) * [Booking a lesson using events - an overview](#psudo-lesson-write-model) * [Extracting CQRS components and refactoring the write model](#extract-refactor-write-model) * [Persisting events to an event store](#persisting-events) * [Listing to events and updating read models](#updating-read-model) * [Improving the read model with relationships](#read-model-relationships) * [Loading lessons](#loading-lessons) * [Protecting against invarients](#invariants) * [Generating UUIDs](#uuids) * ["Enforcing" Read Model immutability](#immutable-models) <a name="introduction"></a> ### Introduction ### This post runs through the basics of creating an event sourced/CQRS system with PHP and Laravel. It assumes familiarity with the command bus design pattern and events (publishing events to an array of listeners). Laracasts have you covered if you need to brush up! It also assumes you have some familiarity with the concept of CQRS. If not, I highly recommend two talks: [Practical Event Sourcing by Mathias Verraes](http://verraes.net/2014/03/practical-event-sourcing) and [CQRS and Event sourcing by Greg Young](https://www.youtube.com/watch?v=JHGkaShoyNs). Please don't use this code for your projects! It's a learning platform to uncover the ideas behind CQRS. It's not robust, it's poorly tested and I'm rarely coding to interfaces which will make it harder to swap parts out. A much better example of a CQRS package you can use is [Qandidate Lab's Broadway](https://github.com/qandidate-labs/broadway). It's decoupled, clean code but some of the abstractions make it hard to follow if you've never seen an event sourced system. @@ -21,7 +27,7 @@ We will be building the start of a booking system for a surf school. It will all - A lesson must have at least one client - No more than 3 clients per lesson One of the exciting things about CQRS and Event Sourced systems is creating read models specific to each metric you require from the system. You can find examples projecting read models into ElasticSearch and Greg Young has a DSL for Complex Event Processing built into his event store. But, in an effort to reduce the learning curve, our read projection will be a standard SQL database you would use with Eloquent. We will end up with one table for lessons, and one for clients. Event Sourcing also gives you the ability to process the events off-line. This post will keep as close to "traditional" development models as possible (again to reduce complexity) and our read projections will be updated in real time, as soon as events are persisted into the event store. @@ -71,7 +77,7 @@ class CQRSTest extends TestCase { We provide the ID for our new lesson up front, create a command to book a new lesson and tell Laravel to dispatch the command. We want a new entry to be created in the lessons table, which we can read with an Eloquent model. We will be needing a database, so fill in your .env file as required. Each event logged to our event store is attached to an Aggregate Root (which we will just call an Entity - the abstraction for learning purposes just adds to the confusion). This ID is a universally unique ID. The event store doesn't care if the event is to be applied to a Lesson or a Client, it just knows it's attached to an ID. <a name="missing-classes"></a> ### Follow the test ### @@ -204,10 +210,10 @@ Following along? Let's take a look at the process this command should initiate: 1. Validation: imperative commands can fail, events are past tense and must not. 2. Generate a new LessonWasBooked event 3. Update the Lesson's state. (The write model needs to be aware of the model's state so it can perform validation) 4. Add this event to a stream of uncommitted events stored on the lesson write model 5. Persist the uncommitted events stream to the event store 6. Fire the LessonWasBooked event globally to inform all read projectors they should update the lessons table @@ -279,7 +285,56 @@ class Lesson { <a name="extract-refactor-write-model"></a> ### Extracting CQRS components and refactoring the write model ### ~~~ Following along? $> git checkout 9c148fe ~~~ We can extract the CQRS components out from our write model - the parts of the class dealing with uncommitted events. We can also clean up the API for an event sourced entity by creating a protected function called apply() which takes an event, calls the appropriate applyEventName() method and appends a new DomainEventMessage to the uncommitted events list. The extracted class is an implementation detail of CQRS, and doesn't contain any domain logic so we can create a new namespace: App\CQRS: ``` <?php namespace App\CQRS; abstract class EventSourcedEntity implements EventSourcedEntityInterface { private $uncommittedEvents = array(); public function apply( $event ) { $this->handle( $event ); $this->uncommittedEvents[] = DomainEventMessage::recordNow( $this->getEntityId(), $event ); } public function getUncommittedDomainEvents() { return $this->uncommittedEvents; } private function handle( $event ) { $method_name = $this->getApplyMethodName($event); if (! method_exists($this, $method_name)) { return; } $this->$method_name($event); } private function getApplyMethodName($event) { $className = get_class($event); $classParts = explode('\\', $className); $methodName = end($classParts); return 'apply'. $methodName; } } ``` To get the code to run, we need to add a DomainEventMessage class, which is just a simple DTO: ``` <?php @@ -332,26 +387,36 @@ class DomainEventMessage { } ``` <a name="persisting-events"></a> ### Persisting events to an event store ### ~~~ Following along? $> git checkout 92ee32e ~~~ So, we have a system which generates events for every write and uses the events to recorded changes necessary to prevent invariants. The next step is to persist these events to the EventStore. First, we need an event store. Keeping things simple, we'll use an Eloquent model, just a simple SQL table with fields for: * UUID (so we know which entity to apply the event to) * event_payload (a serialized message containing everything we need to rebuild the event) * recordedAt - a timestamp so we know when the event occurred If you check the code, you'll see I've created two commands to create and destory our event store table: * php artisan eloquenteventstore:create (App\CQRS\EloquentEventStore\CreateEloquentEventStore) * php artisan eloquenteventstore:drop (App\CQRS\EloquentEventStore\DropEloquentEventStore) (not forgetting to add these to App\Console\Kernel.php so they're loaded) There are two very good reasons not to use an SQL table as your event store: it's not an append-only model (events should be immutable) and SQL is not a great temporal query language. We're coding to an interface, so it will be straight forward to swap the event store in a later blog post. We will use a repository to handle the saving of events. Whenever save() is called for a write model, we persist the list of uncommittedEvents to the event store. To store events, we need to a way to serialize/deserialize them. We can create a Serializer to handle this. We will need meta data, such as the event class (eg App\School\Lesson\Events\LessonWasOpened) and the event payload (data needed to reconstruct the event). All will then be JSON encoded and then written to our database, along with the entity UUID and timestamp. As soon each event is persisted, we will want our read models to update, so the repository will fire each event after saving. The Serializer will be responsible for recording the event class, whilst the event is responsible for the serializing of its payload. A fully serialized event will look something like: ``` { @@ -360,9 +425,9 @@ We need to a way to serialize and deserialize events so they can be written in o } ``` Since all events will need a serialize and deserialize method, we can create a SerializableEvent interface, and start type hinting :) Update our LessonWasOpened event: ``` class LessonWasOpened implements SerializableEvent { @@ -374,7 +439,7 @@ class LessonWasOpened implements SerializableEvent { } ``` Create the LessonRepository. We can refactor and extract generic CQRS stuff later. ``` <?php @@ -387,26 +452,40 @@ use Event; class LessonRepository { /* TODO: Dependency Injection! */ public function __construct() { $this->eventStoreRepository = new EloquentEventStoreRepository( new EventSerializer() ); } public function save(Lesson $lesson) { /** @var DomainEventMessage $domainEventMessage */ foreach( $lesson->getUncommittedDomainEvents() as $domainEventMessage ) { $this->eventStoreRepository->append( $domainEventMessage->getId(), $domainEventMessage->getEvent(), $domainEventMessage->getRecordedAt() ); Event::fire($domainEventMessage->getEvent()); } } } ``` If you run the integration test again, then check the domain_events SQL table, you should see two events in the database. <a name="updating-read-model"></a> ### Listing to events and updating read models ### ~~~ Following along? $> git checkout e202739 ~~~ Our final step to get our test to pass: listening for the broadcasted events and updating the Lesson read model projection. The broadcasted Lesson events will be caught by a LessonProjector, which applies the neccessary changes to a LessonProjection (just an Eloquent model of the lessons table): ``` <?php @@ -432,8 +511,15 @@ class LessonProjector { public function subscribe(Dispatcher $events) { $fullClassName = self::class; $events->listen( LessonWasOpened::class, $fullClassName.'@applyLessonWasOpened' ); $events->listen( ClientBookedOntoLesson::class, $fullClassName.'@applyClientBookedOntoLesson' ); } } ``` @@ -466,15 +552,22 @@ If you run the test, you'll see that there is an SQL error: Unknown column 'clientName' in 'field list' ``` Once we create a migration to add clientName to the lessons table, our test should pass :). We have implemented basic CQRS functionality: commands create events which are used to generate read models. <a name="read-model-relationships"></a> ### Improving the read model with relationships ### ~~~ Following along? $> git checkout 3a70e75e ~~~ We've reach a mile stone, but we're not there yet! The read model only has support for one client (we specified 3 in our domain rules). The changes we make to the read model are fairly straight forward; we just create a Client projection model and ClientProjector which catches the ClientBookedOntoLesson event. First, let's update our test to reflect the changes we want to see in our read model: ``` public function testFiringEventUpdatesReadModel() @@ -494,15 +587,22 @@ public function testFiringEventUpdatesReadModel() } ``` The updated projector, read model and migrations are on github - it's basic laravel stuff. It highlights how easy it is to change your read models. Everything up to the event store remains the same. Smoke testing is something we get for free with an event sourced system - if we change our read model projector, we have a listen of every event that has ever happened in our system. We replay these events using the new projector, check for exceptions and compare the output with the old projections. If the system has been live for some time, we will have a comprehensive list of events to test our projectors with. <a name="loading-lessons"></a> ### Loading lessons ### ~~~ Following along? $> git checkout a10888e ~~~ Our write model currently has no way of loading the state of existing. If we want to add a second client to a lesson, we could just create a second ClientWasAddedToLesson event but we wouldn't be able to protect against invariants. To explain what I mean, let's write a second test simulating booking two clients into a lesson. ``` public function testLoadingWriteModel() @@ -534,9 +634,15 @@ private function assertClientCollectionContains(Collection $clients, $nameToFind } ``` We need a way for our write model to "load" an entity that already has events applying to it in the event store. We can achieve this by replaying every event which refers to the entity's UUID. The basic process is: 1. Get all relevant event messages from the event store 2. For each message, recreate the appropriate event 3. Create a new entity write model and replay each event At the moment, our tests throws exceptions so we start by creating the required BookClientOntoLesson command, using the BookLesson command as a template. The handle method will look like: ``` public function handle(LessonRepository $repository) { @@ -558,7 +664,13 @@ public function load(LessonId $id) { } ``` The repository's load function returns an array recreated events. It does this by first finding the event messages in the event store, then delegating to the Serializer to turn each event message into an event. The Serializer created the messages from events, so we need to add a deserialize() method to reverse the process. You'll remember the Serializer delegated to each event to manage the serialzing of event data (eg client name). We'll do the same when reversing the process so our SerializableEvent interface should be updated with a deserialize() method. Let's take a look at the code to explain things better. First the LessonRepository's load function: ``` public function load($uuid) { @@ -567,7 +679,10 @@ public function load($uuid) { foreach($eventMessages as $eventMessage) { /* We serialized our event into an event_payload, so we need to deserialize before returning */ $events[] = $this->eventSerializer->deserialize( json_decode($eventMessage->event_payload) ); } return $events; @@ -585,7 +700,7 @@ public function deserialize( $serializedEvent ) { } ``` Finally, the static factory deserialize() method in LessonWasOpened (we need to add this method to every event) ``` public static function deserialize($data) { @@ -594,7 +709,7 @@ public static function deserialize($data) { } ``` Now we have an array of all previous events, we just replay them against our Entity write model to initialize state: (Added to App\CQRS\EventSourcedEntity) ``` @@ -605,8 +720,15 @@ public function initializeState($events) { } ``` And rerun our test - we're back to Green! <a name="invariants"></a> ### Protecting against invariants ### ~~~ Following along? $> git checkout eabe2cf ~~~ We don't actually have a test here to ensure we're enforcing our domain rules, so let's write one: @@ -623,9 +745,15 @@ public function testMoreThan3ClientsCannotbeAddedToALesson() { } ``` You'll notice that we only need a lessonId - this test re-initializing the lesson's state with each command. <a name="uuids"></a> ### Generating UUIDs ### ~~~ Following along? $> git checkout eeeac9e ~~~ At the moment, we're just passing in hand crafted UUIDs when really we want to generate these automatically. I'm going to use the Ramsy\UUID package, so let's install that with composer: @@ -643,9 +771,15 @@ public function testEntityCreationWithUUIDGenerator() { } ``` <a name="immutable-models"></a> ### "Enforcing" Read Model immutability ### ~~~ Following along? $> git checkout 5e62320 ~~~ Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel class which extends the Eloquent Model class and overrides the save method: ``` <?php @@ -664,4 +798,4 @@ class ImmutableModel extends Model { } ``` Obviously a determined developer can still update the read model if they want to, but this will help prevent confusion. -
scazz revised this gist
May 12, 2015 . 1 changed file with 37 additions and 9 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,9 +1,10 @@ ### Contents ### * [Introduction](#introduction) * [Project Setup and first test](#project-setup) * [Follow the test](#missing-classes) * [Booking a lesson using events - an overview](#psudo-lesson-write-model) * [Extracting CQRS components and refactoring the write model](#extract-refactor-write-model) <a name="introduction"></a> ### Introduction ### @@ -68,9 +69,9 @@ class CQRSTest extends TestCase { } ``` We provide the ID for our new lesson up front, create a command to book a new lesson and tell Laravel to dispatch the command. We want a new entry to be created in the lessons table, which we can read with an Eloquent model. We will be needing a database, so fill in your .env file as required. Each event logged to our event store is attached to an Aggregate Root (which we will just call an Entity - the abstraction for learning purpses just adds to the confusion). This ID is a universally unique ID. The event store doesn't care if the event is to be applied to a Lesson or a Client, it just knows it's attached to an ID. <a name="missing-classes"></a> ### Follow the test ### @@ -80,7 +81,13 @@ Following along? $> git checkout 284da0f9 ~~~ Being driven by our test errors, we can create our missing classes. First we create a LessonId class, followed by the BookLesson command (don't worry about the handle method yet, just keep following the test). The Lesson class is a read model outside of the Lesson namespace - it will only ever be a read model - no domain logic will reside in here. Finally, we have to create a migration for the lessons table. I'm using an assertion library to keep the code clean. You can grab it with: ``` $> composer require beberlei/assert ``` ``` <?php @@ -187,13 +194,31 @@ class CreateLessonTable extends Migration { } ``` <a name="pseudo-lesson-write-model"></a> ### Booking a lesson using events - an overview ### ~~~ Following along? $> git checkout bd4e01a4 ~~~ Let's take a look at the process this command should initiate: 1. Validation: imperitive commands can fail, events are past tense and must not. 2. Generate a new LessonWasBooked event 3. Update the Lesson's state. (The write model needs to be aware of the model's state so it can perform validation) 4. Add this event to a stream of uncommited events stored on the lesson write model 5. Persist the uncommitted events stream to the event store 6. Fire the LessonWasBooked event globally to inform all read projectors they should update the lessons table So first, we need to create a write model for the lesson. We use a static factory method, Lesson::bookClientOntoNewLesson(). This generates a new LessonWasOpened event, applies the event to itself (just sets it's ID), adds the new event to the uncommitted events list in the form of a DomainEventMessage (the event, plus some meta data which we will use when persisting to the event store). It repeats the process to add a client to the event. When applying the ClientWasBookedOntoLesson event, the write model doesn't keep a track of the client names, just how many clients are booked in. The write model doesn't need to care about client names to protect invariants. The applyLessonWasOpened and applyClientWasBookedOntoLesson methods might seem a little weird at the moment. They will be used later when we need to replay old events to build up the write model's state, It's a tricky concept to explain so here is some code to help outline the process. Later, we will extract out the code handling uncommittedEvents and domain event message creation. ``` <?php @@ -251,6 +276,9 @@ class Lesson { } ``` <a name="extract-refactor-write-model"></a> ### Extracting CQRS components and refactoring the write model ### N.b. Extracting out common parts of this function leave us with a base class we can use for all other entities. I created a new namespaced bundle for the CQRS generics and infrustructure, App\CQRS. The extracted base class is called EventSourcedEntity, and I also created a DomainEventMessage class, just a simple DTO: ``` -
scazz revised this gist
May 11, 2015 . 1 changed file with 31 additions and 15 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,8 +2,8 @@ ### Contents ### * [Introduction](#introduction) * [Project Setup and first test](#project-setup) * [Follow the test](#missing-classes) <a name="introduction"></a> ### Introduction ### @@ -25,7 +25,12 @@ One of the exciting things about CQRS and Event Sourced systems is creating read Event Sourcing also gives you the ability to process the events off-line. This post will keep as close to "traditional" development models as possible (again to reduce complexity) and our read projections will be updated in real time, as soon as events are persisted into the event store. <a name="project-setup"></a> ### Project Set up and first test ### ~~~ Following along? $> git checkout c60aa7b ~~~ Start a new laravel 5 project @@ -48,23 +53,34 @@ class CQRSTest extends TestCase { * Assert the BookLesson Command creates a lesson in our read projection * @return void */ public function testFiringEventUpdatesReadModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440000'; $clientName = "George"; $lessonId = new LessonId($testLessonId); $command = new BookLesson($lessonId, $clientName); $this->dispatch($command); $this->assertNotNull(Lesson::find($testLessonId)); $this->assertEquals( Lesson::find($testLessonId)->clientName, $clientName ); } } ``` We provide the ID for our new lesson up front. Each event logged to our event store is attached to an Aggregate Root (which we will just call an Entity - the abstraction for learning purpses just adds to the confusion). This ID is a universally unique ID. The event store doesn't care if the event is to be applied to a Lesson or a Client, it just knows it's attached to an ID. We will be needing a database too, so fill in your .env file as required. <a name="missing-classes"></a> ### Follow the test ### ~~~ Following along? $> git checkout 284da0f9 ~~~ Being driven by our tests errors, we can create our missing classes. You'll notice the Lesson model is namespaced into ReadProjections. The eloquent model here is created from our event stream and is just for reading. It won't contain any domain invariant logic. ``` <?php -
scazz revised this gist
May 11, 2015 . 1 changed file with 8 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,11 @@ # Event Sourcing, CQRS and Laravel # ### Contents ### * [Introduction](#introduction) * [Project Setup](#project-setup) <a name="introduction"></a> ### Introduction ### This post runs through the basics of creating an event sourced/cqrs system with PHP and Laravel. It assumes familiarity with the command bus design pattern and events (publishing events to an array of listeners). Laracasts have you covered if you need to brush up! It also assumes you have some familiarity with the concept of CQRS. If not, I highly recommend two talks: [Practical Event Sourcing by Mathias Verraes](http://verraes.net/2014/03/practical-event-sourcing) and [CQRS and Event sourcing by Greg Young](https://www.youtube.com/watch?v=JHGkaShoyNs). @@ -14,10 +20,11 @@ We will be building the start of a booking system for a surf school. It will all - A lesson must have at least one client - No more than 3 clients per lesson One of the exciting things about CQRS and Event Sourced systems is creating read models specific to each metric you require from the system. You can find examples projecting read models into ElasticSearch and Greg Young has a DSL for Complex Event Proessing built into his event store. But, in an effort to reduce the learning curve, our read projection will be a standard SQL database you would use with Eloquent. We will end up with one table for lessons, and one for clients. Event Sourcing also gives you the ability to process the events off-line. This post will keep as close to "traditional" development models as possible (again to reduce complexity) and our read projections will be updated in real time, as soon as events are persisted into the event store. <a name="project-setup"></a> ### Project Set up ### Start a new laravel 5 project -
scazz revised this gist
May 11, 2015 . 1 changed file with 36 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,14 +1,22 @@ # Event Sourcing, CQRS and Laravel # ### Introduction ### This post runs through the basics of creating an event sourced/cqrs system with PHP and Laravel. It assumes familiarity with the command bus design pattern and events (publishing events to an array of listeners). Laracasts have you covered if you need to brush up! It also assumes you have some familiarity with the concept of CQRS. If not, I highly recommend two talks: [Practical Event Sourcing by Mathias Verraes](http://verraes.net/2014/03/practical-event-sourcing) and [CQRS and Event sourcing by Greg Young](https://www.youtube.com/watch?v=JHGkaShoyNs). Please don't use this code for your projects! It's a learning platform to uncover the ideas behind CQRS. It's not robust, it's poorly tested and I'm rarely coding to interfaces which will make it harder to swap parts out. A much better example of a CQRS package you can use is [Qandidate Lab's Broadway](https://github.com/qandidate-labs/broadway). It's decoupled, clean code but some of the abstractions make it hard to follow if you've never seen an event sourced system. Finally, this code is coupled to Laravel's Events and Command Bus. I wanted to see what the code would look like with Laravel (it's my framework of choice for smaller agency projects), but looking back I should have created my own implementations. I hope people will be able to follow along, even if they don't use a framework. The code is on github: [https://github.com/scazz/cqrs-tutorial.git](https://github.com/scazz/cqrs-tutorial.git), and the tutorial takes you through the code, commit by commit. Hopefully this will make it easy to follow along! We will be building the start of a booking system for a surf school. It will allow clients to book onto lessons. Our domain rules for this process are: - A lesson must have at least one client - No more than 3 clients per lesson One of the exciting things about CQRS and Event Sourced systems is creating read models specific to each metric you require from the system. You can find examples projecting read models into ElasticSearch and Greg Young has a DSL for Complex Event Proessing built into his event store. But, in an effort to reduce the learning curve, our read projection will be a standard SQL database you would use with Eloquent. We will end up with one table for lessons, and one for clients. Event Sourcing also gives you the ability to process the events off-line. This post will keep as close to "traditional" development models as possible (again to reduce complexity) and our read projections will be updated in real time, as soon as events are persisted into the event store. ### Project Set up ### @@ -583,3 +591,26 @@ public function testEntityCreationWithUUIDGenerator() { $this->assertInstanceOf( Lesson::class, Lesson::find( (string) $lessonId) ); } ``` ### "Enforcing" Read Model immutibility ### Currently, a new developer to a project could look at the code, see App\School\ReadModels which contains a set of Eloquent models and use these models to write changes to the lessons table. We can stop this by creating an ImmutableModel classwhich extends the Eloquent Model class and override the save method: ``` <?php namespace App\CQRS\ReadModel; use Illuminate\Database\Eloquent\Model; class ImmutableModel extends Model { public function save(array $options = array()) { throw new SavingImmutableModel( "Generate events in order to change this model!" ); } } ``` Obviously a determined developer can still update the read model if they want to, but this will help prevent confusion. -
scazz revised this gist
May 11, 2015 . 1 changed file with 175 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -407,5 +407,179 @@ If you run the test, you'll see that there is an SQL error: Unknown column 'clientName' in 'field list' ``` Once we create a migration to add clientName to the lessons table, our test should pass :). We have implemented the basic functionality to update an Eloquent model with Event Sourcing. ### A read model with relationships ### We've reach a marker stone, but we're not there yet! The read model only has support for one client (we specified 3 in our domain rules). The changes we make to the read model are fairly straight forward; we just create a Client projection model and client projector which catches the ClientBookedOntoLesson event. First, let's change our test to reflect the changes we want to see in our read model: ``` public function testFiringEventUpdatesReadModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440000'; $clientName = "George"; $lessonId = new LessonId($testLessonId); $command = new BookLesson($lessonId, $clientName); $this->dispatch($command); $lesson = Lesson::find($testLessonId); $this->assertEquals( $lesson->id, $testLessonId ); $client = $lesson->clients()->first(); $this->assertEquals($client->name, $clientName); } ``` I won't include the code here to change the read model - it's basic laravel, but you can check the code on github. This step is here so you can see how easy it is to change your read model - no changes to the commands / events /event store were required, we just change how our read model is generated. Smoke testing is something we get for free with an event sourced system - if we change our read model projector, we have a listen of every event that has ever happened in our system. We can re-run these events against our new projector, check for exceptions and compaire the output with the old projections. If the system has been live for some time, we will have a comprehensive list of events to test our projectors with. ### Loading lessons ### Our write model currently has no way of loading the state of previously opened lessons. If we want to add a second client to a lesson, we could just fire a second ClientWasAddedToLesson event but we wouldn't be able to protect against invarients. Let's write a second test simulating how we want to add a second client to an open lesson: ``` public function testLoadingWriteModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440001'; $lessonId = new LessonId($testLessonId); $clientName_1 = "George"; $clientName_2 = "Fred"; $command = new BookLesson($lessonId, $clientName_1); $this->dispatch($command); $command = new BookClientOntoLesson($lessonId, $clientName_2); $this->dispatch($command); $lesson = Lesson::find($testLessonId); $this->assertClientCollectionContains($lesson->clients, $clientName_1); $this->assertClientCollectionContains($lesson->clients, $clientName_2); } private function assertClientCollectionContains(Collection $clients, $nameToFind) { $attributeArray = []; foreach( $clients as $object ) { $attributeArray[] = $object->name; } $this->assertTrue( in_array($nameToFind, $attributeArray), "Could not find client named: ${nameToFind}" ); } ``` We need a way for our write model to "load" an entity where there are already events in the event store which have altered it's state. We can atchieve this by pulling out every event which refers to the entity's UUID, then replaying each event in order. We can start by creating the required BookClientOntoLesson command, using the BookLesson command as a template. Our handle method will look something like: ``` public function handle(LessonRepository $repository) { /** @var Lesson $lesson */ $lesson = $repository->load($this->lessonId); $lesson->addClient($this->clientName); $repository->save($lesson); } ``` Add the load event in the lesson repository: ``` public function load(LessonId $id) { $events = $this->eventStoreRepository->load($id); $lesson = new Lesson(); $lesson->initializeState($events); return $lesson; } ``` Our repositories load function pulls out all event messages in the event store which are associated with our entity, and asks the eventSerializer to build an event out of the serialized payloads. The event serializer pulls out the event class from the payload, and creates a new event. Finally, the serializer tells the new event to rebuild itself. ``` public function load($uuid) { $eventMessages = EloquentEventStoreModel::where('uuid', $uuid)->get(); $events = []; foreach($eventMessages as $eventMessage) { /* We serialized our event into an event_payload, so we need to deserialize before returning */ $events[] = $this->eventSerializer->deserialize( json_decode($eventMessage->event_payload)); } return $events; } ``` With the corresponding deserialize function in the eventSerializer: ``` public function deserialize( $serializedEvent ) { $eventClass = $serializedEvent->class; $eventPayload = $serializedEvent->payload; return $eventClass::deserialize($eventPayload); } ``` finally, create the static factory function deserialize in LessonWasOpened (every event needs to have a deserialized function, so add the method to the SerializableEvent interface ) ``` public static function deserialize($data) { $lessonId = new LessonId($data->lessonId); return new self($lessonId); } ``` Now we have an array of all previous events, we just re-run them against our Entity write model to initialize stae: (Added to App\CQRS\EventSourcedEntity) ``` public function initializeState($events) { foreach( $events as $event ) { $this->handle($event); } } ``` ### Verify we protect against invarients ### We don't actually have a test here to ensure we're enforcing our domain rules, so let's write one: ``` public function testMoreThan3ClientsCannotbeAddedToALesson() { $testLessonId = '123e4567-e89b-12d3-a456-426655440002'; $lessonId = new LessonId($testLessonId); $this->dispatch( new BookLesson($lessonId, "bob") ); $this->dispatch( new BookClientOntoLesson($lessonId, "george") ); $this->dispatch( new BookClientOntoLesson($lessonId, "fred") ); $this->setExpectedException( TooManyClientsAddedToLesson::class ); $this->dispatch( new BookClientOntoLesson($lessonId, "emma") ); } ``` You'll notice that we only need a lessonId - this test also verifies we're re-initializing the lesson's state with each command. ### Generating IDs ### At the moment, we're just passing in hand crafted UUIDs when really we want to generate these automatically. I'm going to use the Ramsy\UUID package, so let's install that with composer: ``` $> composer require ramsey/uuid ``` And update our tests to use the new package: ``` public function testEntityCreationWithUUIDGenerator() { $lessonId = new LessonId( (string) \Rhumsaa\Uuid\Uuid::uuid1() ); $this->dispatch( new BookLesson($lessonId, "bob") ); $this->assertInstanceOf( Lesson::class, Lesson::find( (string) $lessonId) ); } ``` -
scazz revised this gist
May 9, 2015 . 1 changed file with 189 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -220,4 +220,192 @@ class Lesson { } ``` N.b. Extracting out common parts of this function leave us with a base class we can use for all other entities. I created a new namespaced bundle for the CQRS generics and infrustructure, App\CQRS. The extracted base class is called EventSourcedEntity, and I also created a DomainEventMessage class, just a simple DTO: ``` <?php namespace App\EventSourcing; use DateTime; class DomainEventMessage { private $id; private $event; /** * @var DateTime */ private $dateTime; public function __construct($id, $event, DateTime $dateTime) { $this->id = $id; $this->event = $event; $this->dateTime = $dateTime; } public static function recordNow($id, $event ) { return new DomainEventMessage($id, $event, new DateTime()); } /** * @return DateTime */ public function getDateTime() { return $this->dateTime; } /** * @return mixed */ public function getEvent() { return $this->event; } /** * @return mixed */ public function getId() { return $this->id; } } ``` If you check out the code on github, the next tag I've added missing classes and refactored our Lesson class to inherit from EventSourcedEntity. So, we have a system which generates events for every write and uses the events to recorded changes nessessary to prevent invarients. The next steps are to persist these events to the EventStore and broadcast the events to our ReadModel projectors. First, we need an event store. To begin with, we'll use an Eloquent model, just a simple SQL table with fields for: uuid (so we know which entity to apply the event to) event_payload (a serialized message containing everything we need to rebuild the event) recordedAt - a timestamp so we know when the event occured If you check the code, you'll see I've created two commands to create and destory our event store table: php artisan eloquenteventstore:create (App\CQRS\EloquentEventStore\CreateEloquentEventStore) php artisan eloquenteventstore:drop (App\CQRS\EloquentEventStore\DropEloquentEventStore) (not forgetting to add these to App\Console\Kernel.php so they're loaded) There are a lot of reasons not to use an SQL table as your event store... It's not an append-only model, you don't have a DSL query language etc. But for learning, it will work just fine. Later, we will move across to Greg Young's EventStore. We will use a repository to handle the saving of events. Whenever save() is called for our Lesson write model, we will save the list of uncommittedEvents to the event store. We only want our read model to know about persisted events, so we will also broadcast this list of events here. We need to a way to serialize and deserialize events so they can be written in our event store. We will create a Serializer class which will record the event class, and then ask the event to serialize itself and add that to a payload. We will json_encode the resulting array when we write it into our database. So the payload will look something like: ``` { class: "App\\School\\Lesson\\Events\\<MyEvent>", event: $event->serialize() } ``` This also means we can create a SerializableEvent interface, and start type hinting (all events we write to the event store must know how to serialize and deserialize themselves). We update our LessonWasOpened event: ``` class LessonWasOpened implements SerializableEvent { public function serialize() { return array( 'lessonId'=> (string) $this->getLessonId() ); } } ``` Here is the non-abstracted LessonRepository. We will then refactor, so LessonRepository inherrits of a generic EventSourcedEntityRepository (which you can see in the code on github): ``` <?php namespace App\School\Lesson; use App\CQRS\DomainEventMessage; use App\CQRS\EloquentEventStore\EloquentEventStoreRepository; use App\CQRS\Serializer\EventSerializer; use Event; class LessonRepository { public function __construct() { $this->eventStoreRepository = new EloquentEventStoreRepository( new EventSerializer() ); } public function save(Lesson $lesson) { /** @var DomainEventMessage $domainEventMessage */ foreach( $lesson->getUncommittedDomainEvents() as $domainEventMessage ) { $this->eventStoreRepository->append( $domainEventMessage->getId(), $domainEventMessage->getEvent(), $domainEventMessage->getRecordedAt() ); Event::fire($domainEventMessage->getEvent()); } } } ``` If you run the integration test, then check the domain_events sql table, you should see two events in the database. Our final step to get our test to pass: catching our events and projecting them into our Lesson read model. Our Lesson events should be caught by a LessonProjector, a class which applies the neccessary changes to a LessonProjection. The LessonProjection, in this case, is just an Eloquent Model of the lessons table: ``` <?php namespace App\School\Lesson\Projections; use App\School\Lesson\Events\ClientBookedOntoLesson; use App\School\Lesson\Events\LessonWasOpened; use Illuminate\Events\Dispatcher; class LessonProjector { public function applyLessonWasOpened( LessonWasOpened $event ) { $lessonProjection = new LessonProjection(); $lessonProjection->id = $event->getLessonId(); $lessonProjection->save(); } public function applyClientBookedOntoLesson( ClientBookedOntoLesson $event ) { $lessonProjection = LessonProjection::find($event->getLessonId()); $lessonProjection->clientName = $event->getClientName(); $lessonProjection->save(); } public function subscribe(Dispatcher $events) { $fullClassName = self::class; $events->listen( LessonWasOpened::class, $fullClassName.'@applyLessonWasOpened'); $events->listen( ClientBookedOntoLesson::class, $fullClassName.'@applyClientBookedOntoLesson'); } } ``` ``` <?php namespace App\School\Lesson\Projections; use Illuminate\Database\Eloquent\Model; class LessonProjection extends Model { public $timestamps = false; protected $table = "lessons"; } ``` And obviously, don't forget to register the event subscriber with laravel: App\Providers\EventServiceProvider.php: ``` public function boot(DispatcherContract $events) { parent::boot($events); Event::subscribe( new LessonProjector() ); } ``` If you run the test, you'll see that there is an SQL error: ``` Unknown column 'clientName' in 'field list' ``` Once we create a migration to add clientName to the lessons table, our test should pass :). We have implemented the basic functionality to update an Eloquent model with Event Sourcing. -
scazz revised this gist
May 8, 2015 . 1 changed file with 214 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,14 +1,223 @@ # Event sourcing and CQRS # ### Introduction ### This post with run through the basics of creating an event sourced/cqrs system on top of laravel. It assumes familiarity with the command bus design pattern and events (publishing events to an array of listeners). It also assumes you have some familiarity with the concept of CQRS. If not, I highly recommend two talks: [Practical Event Sourcing by Mathias Verraes](http://verraes.net/2014/03/practical-event-sourcing) and [CQRS and Event sourcing by Greg Young](https://www.youtube.com/watch?v=JHGkaShoyNs). The system we create isn't a system I would advocate using in a production environment. It's primarily a learning platform, it's only integration tested and it's tightly coupled to Eloquent and Laravel's command bus and eventing implementations. If you're looking for a more production ready library, check out [Qandidate Lab's Broadway](https://github.com/qandidate-labs/broadway). The code is great, it looks easy to extends and there are symfony and laravel packages out there (though I haven't looked at them). Code is all on git hub, and I've tried to tag sensibly so you should be able to follow along. The system we're building is a booking system. A lesson requires at least 1 student to exist and each lesson can have up to 3 students. People advocating CQRS talk a lot about the benefits of generating read projections for each metric you want to pull out from the code - this is *really* cool. Projecting read models into ElasticSearch can give you incredibly powerful queries. But, I'm a huge fan of SQL and the majority of "agency style" projects simply don't need this. The great thing about event sourcing, is that we can very quickly and cleanly bolt this on at the end of the project, if a client expresses an interest. Our "read projection" will be the standard table you would expect to use with Eloquent models. You will also hear people say "Event Sourcing doesn't have to be real time". This is a really powerful concept if you're google. But most "agency" projects should be real time. When you make a request to a page to change something, they want to see these changes straight away. So, whenever an even is fired, the projection (our Lesson SQL table) will be updated instantly. ### Project Set up ### Start a new laravel 5 project ``` $> laravel new cqrs-tutorial ``` And the first thing we need is a test. We're just going to stick with an integration test to assert that booking a lesson for a client results in that lesson being created in our Eloquent model. ``` <?php use Illuminate\Foundation\Bus\DispatchesCommands; class CQRSTest extends TestCase { use DispatchesCommands; /** * Assert the BookLesson Command creates a lesson in our read projection * @return void */ public function testFiringEventUpdatesReadModel() { $testLessonId = '123e4567-e89b-12d3-a456-426655440000'; $clientName = "George"; $lessonId = new LessonId($testLessonId); $command = new BookLesson($lessonId, $clientName); $this->dispatch($command); $this->assertEquals( Lesson::find($testLessonId)->clientName, $clientName ); } } ``` We provide the ID for our new lesson up front. Each event logged to our event store is attached to an Aggregate Root (which we will just call an Entity - the abstraction for learning purpses just adds to the confusion). This ID is a universally unique ID. The event store doesn't care if the event is to be applied to a Lesson or a Client, it just knows it's attached to an ID. Following the test errors, we can fill in our missing classes (don't forget to add the neccessary use declerations to the test case). It's an integration test, so you will need to set up the database as well. You'll notice the Lesson model is namespaced into ReadProjections. The eloquent model here is created from our event stream and is just for reading. It won't contain any domain invariant logic. ``` <?php namespace App\School\Lesson; use Assert\Assertion as Assert; class LessonId { private $lessonId; public function __construct($lessonId) { Assert::string( $lessonId ); Assert::uuid( $lessonId ); $this->lessonId = $lessonId; } /** * @return string */ public function __toString() { return $this->lessonId; } } ``` ``` <?php namespace App\School\Lesson\Commands; use App\Commands\Command; use App\School\Lesson\LessonId; use Illuminate\Contracts\Bus\SelfHandling; class BookLesson extends Command implements SelfHandling { /** @var LessonId */ private $lessonId; public function __construct(LessonId $lessonId) { $this->lessonId = $lessonId; } /** * @return LessonId */ public function getLessonId() { return $this->lessonId; } public function handle() { //TODO: handle dispatched command } } ``` ``` <?php namespace App\School\ReadModels; use Illuminate\Database\Eloquent\Model; class Lesson extends Model { } ``` ``` <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateLessonTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('lessons', function(Blueprint $table) { $table->string('id'); // uuid }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('lesson'); } } ``` OK, cool. Our command is despatched but it doesn't currently do anything. First, we ask the Lesson write model to create a "LessonWasBooked" event. (The write model will also run this event on itself in the process.) Then we add the event to a list of uncommited events, stored on the Lesson write object. Finally, save the list of uncommitted events into our event store, firing each one globally to let our read projectors know they need to update our Lesson read model (in this case our Eloquent lesson table). We ask the write model to create a LessonWasBooked event for the same reason that the write model runs the event on itself. The BookLesson command is written in the imperitive tense and can fail, but the LessonWasBooked event is in the past and must not fail. The write model should check invarients before firing the event. The event is applied to itself because checking the invarients requires the write model to be aware of it's state. It needs to know how many pupils are booked onto a lesson so it can prevent more than 3 pupils registering. So, to start, we will create a Lesson write model, with a static factory method to start the booking process. The static factory creates a new event, applies the event to itself and then adds it to a list of uncommitted events. Saving the write model will add the uncommitted events to the event store and fire each event so we can update our read model. It's a tricky concept to explain so here is some higher level code to help outline the process. We can extact the common calls to apply, and the domain event message creation. A domain event message is an object that can be written to our event store. Contains all the infomation we need to generate the same event again. ``` <?php namespace App\School\Lesson; class Lesson { private $lessonId; private $numberOfClients; private $uncommittedEvents = []; public static function bookClientOntoNewLesson(LessonId $lessonId, $clientName) { $lesson = new Lesson(); $lesson->openLesson( $lessonId ); $lesson->bookClient( $clientName ); return $lesson; } private function openLesson( LessonId $lessonId ) { /* here we would check any invarients - but we don't have any to protect, so we can just generate the events */ $event = new LessonWasOpened( $lessonId); $this->applyLessonWasOpened($event); $this->uncommittedEvents[] = DomainEventMessage::recordNow( $this->lessonId, $event ); } private function applyLessonWasOpened( $event ) { $this->lessonId = $event->lessonId; $this->numberOfClients = 0; } public function bookClient( $clientName ) { if ($this->numberOfClients >= 3) { throw new Exception("Too many clients"); } $event = new ClientBookedOntoLesson( $this->lessonId, $clientName); $this->applyClientBookedOntoLesson( $event ); $this->uncommittedEvents[] = DomainEventMessage::recordNow( $this->lessonId, $event ); } /* * Here, we only keep track of the number of clients - * this is the only thing the write model cares about * * If a domain rules was "no clients can have the same name", * we would need to keep a track of client names. */ private function applyClientBookedOntoLesson( $event ) { $this->numberOfClients++; } } ``` N.b. Extracting out common parts of this function leave us with a base class we can use for all other entities. I created a new namespaced bundle for packages generics and infrustructure, App\CQRS. The base class was named EventSourcedEntity. Check out the code on github to see how. -
scazz revised this gist
May 8, 2015 . 2 changed files with 14 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,14 @@ Event sourcing and CQRS This post with run through the basics of creating an event sourced/cqrs system on top of laravel. It assumes familiarity with the command bus design pattern and events (publishing events to an array of listeners). It also assumes you have some familiarity with the concept of CQRS. If not, I highly recommend two talks: Practical Event Sourcing by Mathias Verraes (http://verraes.net/2014/03/practical-event-sourcing) and CQRS and Event sourcing by Greg Young (https://www.youtube.com/watch?v=JHGkaShoyNs). The system we create isn't a system I would advocate using in a production environment. It's primarily a learning platform, it's only integration tested and it's tightly coupled to Eloquent and Laravel's command bus and eventing implementations. If you're looking for a more production ready library, check out Qandidate Lab's Broadway (https://github.com/qandidate-labs/broadway). The code is great, it looks easy to extends and there are symfony and laravel packages out there (though I haven't looked at them). Code is all on git hub, and I've tried to tag sensibly so you should be able to follow along. The system we're building is a booking system. A lesson requires at least 1 student to exist and each lesson can have up to 3 students. People advocating CQRS talk a lot about the benefits of generating read projections for each metric you want to pull out from the code - this is *really* cool. Projecting read models into ElasticSearch can give you incredibly powerful queries. But, I'm a huge fan of SQL and the majority of "agency style" projects simply don't need this. The great thing about event sourcing, is that we can very quickly and cleanly bolt this on at the end of the project, if a client expresses an interest. Our "read projection" will be the standard table you would expect to use with Eloquent models. You will also hear people say "Event Sourcing doesn't have to be real time". This is a really powerful concept if you're google. But most "agency" projects should be real time. When you make a request to a page to change something, they want to see these changes straight away. So, whenever an even is fired, the projection (our Lesson SQL table) will be updated instantly. -
scazz created this gist
May 8, 2015 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1 @@ publish me? This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1 @@ published: true