Learning Event-Sourcing (or DDD) is tough! i
Be kind to learners (including yourself)
Event-Sourcing isn't for your whole application
The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.
Martin Fowler
What happened?
BookWasCheckedOut
What do I need to remember about it?
(book, patron, date)
(book id, patron id, date)
<?php
namespace Library\Events;
use Library\Support\Event;
class BookWasCheckedOut
{
/**
* @var DateTime
*/
protected $checkoutDate;
/**
* @var int
*/
protected $patronId;
/**
* @var int
*/
protected $bookId;
public function __construct(DateTime $checkoutDate, PatronId $patronId, BookId $bookId)
{
$this->checkoutDate = $checkoutDate;
$this->patronId = $patronId;
$this->bookId = $bookId;
}
/**
* @return array
*/
public function serialize()
{
return [
'checkout_date' => $this->$checkoutDate->toString(),
'patron_id' => $this->patronId->toNative(),
'book_id' => $this->bookId->toNative(),
];
}
/**
* @param array $data
*
* @return static
*/
public static function deserialize($data)
{
return new BookWasCheckedOut(
DateTime::createFromFormat('j-M-Y', $data['checkout_date']),
PatronId::fromNative($data['patron_id']),
BookId::fromNative($data['book_id'])
);
}
/**
* @return int
*/
public function getBookId()
{
return $this->bookId;
}
/**
* @return DateTime
*/
public function getCheckoutDate(): DateTime
{
return $this->checkoutDate;
}
/**
* @return int
*/
public function getPatronId(): int
{
return $this->patronId;
}
}
<?php
namespace Library\ReadModel;
use Library\Events\BookWasCheckedIn;
use Library\Events\BookWasCheckedOut;
use Library\Events\BookAddedToBookshelf;
use App\Support\ReadModel\Replayable;
use App\Support\ReadModel\SimpleProjector;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Connection;
class BookshelfProjector extends SimpleProjector implements Replayable
{
/**
* @var Connection
*/
private $connection;
/**
* @var string table we're playing events into
*/
private $table = 'proj_bookshelf';
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function beforeReplay()
{
$builder = $this->connection->getSchemaBuilder();
$builder->dropIfExists('proj_bookshelf');
$builder->create('proj_bookshelf_tmp', function (Blueprint $schema) {
$schema->string('book_id');
$schema->string('book_title');
$schema->string('book_author');
$schema->string('status');
$schema->string('checkout_date');
$schema->string('patron_id');
$schema->primary('book_id');
});
$this->table = 'proj_bookshelf_tmp';
}
public function afterReplay()
{
$builder = $this->connection->getSchemaBuilder();
$builder->dropIfExists('proj_bookshelf');
$builder->rename('proj_bookshelf_tmp', 'proj_bookshelf');
$this->table = 'proj_bookshelf';
}
/**
* @param BookWasCheckedOut $event
*/
public function applyBookWasCheckedOut(BookWasCheckedOut $event)
{
$bookshelfItem = BookshelfItem::where('id', $event->bookId);
$book->status = 'Checked Out';
$book->checkout_date = $event->checkoutDate;
$book->patron_id = $event->patronId;
$book->save();
}
/**
* @param BookAddedToBookshelf $event
*/
public function applyBookAddedToBookshelf(BookAddedToBookshelf $event)
{
$bookshelfItem = new BookshelfItem();
$bookshelfItem->setTable($this->table);
$bookshelfItem->bookId = $event->bookId;
$bookshelfItem->bookTitle = $event->bookTitle;
$bookshelfItem->bookAuthor = $event->bookAuthor;
$bookshelfItem->status = 'on shelf';
$bookshelfItem->save();
}
/**
* @param BookWasReturned $event
*/
public function applyBookWasReturned(BookWasReturned $event)
{
$bookshelfItem = BookshelfItem::where('id', $event->bookId);
$book->status = 'Available';
$book->checkout_date = '';
$book->patron_id = '';
$book->save();
}
}
A set of event handlers that work together to build and maintain a table to be accessed by the read model.
<?php
namespace Library\ReadModel;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* @codeCoverageIgnore
*/
class Bookshelf extends Model
{
protected $table = 'proj_bookshelf';
public $incrementing = false;
public $timestamps = false;
public static function lookupLoansFor($patronId)
{
return static::where('patron_id', $patronId)->get();
}
public function lookupAvailableBooks()
{
return static::where('status', 'Available')->get();
}
public function lookupOverdueBooks()
{
return static::where('checkout_date', '<', date('Y-m-d', strtotime('-7 days')))->get();
}
}
An application architecture pattern commonly used with event sourcing
CQRS involves splitting an application into two parts internally.
public function update(Request $request)
{
// $request has book id, patron id
try {
$command = new CheckOutBook($request->bookId, $request->patronId);
$this->bookLendingService->handleCheckOutBook($command);
} catch (InvalidUserException $e) {
return response()->json("Not authorized to check out a book.", Response::HTTP_FORBIDDEN);
} catch (BookUnavailableException $e) {
return response()->json("Book was not available to be checked out", 400);
}
<------ dynamic command handler ------>
private function handle(Command $command)
{
$method = $this->getHandleMethod($command);
if (! method_exists($this, $method)) {
return;
}
$this->$method($command);
}
private function getHandleMethod(Command $command)
{
return 'handle' . class_basename($command);
}
<-------- handle check out book command --------->
public function handleCheckoutOutBook(CheckOutBook $command)
{
$book = Book::findOrFail($command->bookId);
$patron = Patron::findOrFail($command->patronId);
if (!$book->isAvailable()) {
throw new BookUnavailableException();
}
if (!$patron->isAuthorized()) {
throw new InvalidUserException();
}
//record the event
$this->record(
new BookWasCheckedOut(date("Y-m-d H:i:s"),
$patron->getId(),
$book->getId())
);
}
A lot of our systems were built to replace paper processes
They often closely map to this physical form.
Status labels are like a rubber stamp
Status doesn't always communicate why or what happened
Piles indicate status of the form
Status is a reflection of something that happened
There is ONE of each status + reasons/details
Events can record what happened