Projections and Read Models
Projections and read models are a big part of event sourcing. They are a way to communicating state with the outside world. They’re also very project-specific.
In EventSauce projections are nothing more than the implementation of
the MessageConsumer
interface.
<?php
namespace EventSauce\EventSourcing;
interface MessageConsumer
{
public function handle(Message $message);
}
The MessageConsumer
accepts a Message
via the handle
method. It can
then retrieve the event from the Message
to read information
about something important that happened in the business.
Why read models are important/useful
Read models allow you to separate presentational state from the process you’re modeling. In general this has two effects. Your processing side is leaner because it doesn’t have to deal with any presentation data or associated presentation logic. On the other your read models are free from any constraints your domain model has and can be very optimized.
Because projections and read models are fed by a stream of events it’s also a lot easier to create multiple read models. These read models can even be specific to one use-case and don’t have to share restrictions.
Read model example: friendship requests
As an example we’re going to create a couple of read models for the following case: becoming friends on social media. In this case we’ll have the following events defined:
FriendshipRequestWasSent
FriendshipRequestWasCancelled
FriendshipRequestWasAccepted
FriendshipRequestWasDenied
All these events describe something that happened in the process of becoming friends on social media.
In our UI we might have two views. One for outgoing invitations and one for incoming. We can map these UI’s to two read models:
OutgoingInvitations
IncomingInvitations
These two read models could very well be placed in a single read model, but for the purpose of our demonstration we’ll separate them.
For each of the read models we can add a projection:
<?php
use EventSauce\EventSourcing\MessageConsumer;
use EventSauce\EventSourcing\Message;
class PendingInvitationProjection implements MessageConsumer
{
public function __construct(PendingInvitations $invitations)
{
$this->invitations = $invitations;
}
public function handle(Message $message): void
{
$event = $message->payload();
if ($event instanceof FriendshipRequestWasSent) {
$this->invitations->add(new FriendshipRequest(
$event->requestId(),
$event->fromUser(),
$event->toUser(),
RequestStatus::pending()
));
} elseif ($event instanceof FriendshipRequestWasCancelled) {
$this->invitations->updateStatus($event->requestId(), RequestStatus::cancelled());
} elseif ($event instanceof FriendshipRequestWasAccepted) {
$this->invitations->updateStatus($event->requestId(), RequestStatus::accepted());
} elseif ($event instanceof FriendshipRequestWasDenied) {
// Just remove the request to prevent sad feelings.
$this->invitations->removeRequest($event->requestId());
}
}
}
Event Consumers
In the previous example we had to manually check the type of the event to react
appropriately. EventConsumer
makes it easier to handle different event types.
The default strategy takes the class name of the event and transforms it to a
method name. For example, FriendshipRequestWasSent
becomes
handleFriendshipRequestWasSent
.
<?php
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
class PendingInvitationProjection extends EventConsumer
{
public function handleFriendshipRequestWasSent(FriendshipRequestWasSent $event, Message $message): void
{
$this->invitations->add(new FriendshipRequest(
$event->requestId(),
$event->fromUser(),
$event->toUser(),
RequestStatus::pending()
));
}
}
A different strategy uses reflection to determine which handler methods accept the event type. This allows you to handle multiple events in a single method using a union type and multiple methods for a single event.
<?php
use EventSauce\EventSourcing\MessageConsumer;
use EventSauce\EventSourcing\EventConsumption\EventConsumer;
use EventSauce\EventSourcing\EventConsumption\HandleMethodInflector;
use EventSauce\EventSourcing\EventConsumption\InflectHandlerMethodsFromType;
class PendingInvitationProjection extends EventConsumer
{
protected function handleMethodInflector(): HandleMethodInflector
{
return new InflectHandlerMethodsFromType();
}
// Note, the handler methods must be public.
public function addInvitation(FriendshipRequestWasSent $event, Message $message): void
{
$this->invitations->add(new FriendshipRequest(
$event->requestId(),
$event->fromUser(),
$event->toUser(),
RequestStatus::pending()
));
}
public function increaseResponseCount(FriendshipRequestWasAccepted|FriendshipRequestWasDenied $event, Message $message): void
{
$this->responses++;
}
}