Adding metadata to Broadway events

At SIMgroep we’re great fans of Broadway, the library originally
developed
at Qandidate to support CQRS and event sourcing in PHP projects. Several of our products are event sourced and Broadway plays a great part in how we made that work.

While most of our projects revolve around end user facing applications, which means they’re intended to be used by citizens, we also have some applications that support the work of civil servants. In one of those applications, we recently realized we wanted to know “who did what” on an event level. We wanted to be able to find out which (logged in) user initiated an event, so we could, optionally, store that information inside the resulting read model.

Now, we could have opted to add a ‘user’, ‘author’ or ‘initiator’ inside each of the commands and each of the events, so we could simply include the user information in our calls. But that has some drawbacks. For instance, any command and event would need to have a user field alongside the fields that are actually part of the domain. While in some cases that might make perfect sense, in most cases it doesn’t. It just pollutes the code, having to toss around a user object or some reference in all communications. Another drawback is that to add the user information to existing events changes the events, which makes replaying those events at a later moment harder. And you’d have to keep in mind to add the user reference to all new events you’re creating; you wanna bet you’re not going to forget that every other time?

It’s much cleaner, easier and way more stable to add this information to the metadata of the event, and have Broadway’s event stream decorator take care of making that happen. When you do that, the user reference can still be stored inside the event, but the event itself isn’t dependent on having a user reference present (because we’re only looking to be able to show the user information in a read model). You don’t have to explicitly configure the events to accept user references inside their metadata either: the decorator can be configured to do this automatically. It’s just a few steps to achieve this, and because it’s both very simple and not overly documented, it seemed appropriate to share how we did it.

Adding user data to each event

When I say ‘each event’ or ‘all events’, I mean all of the events that run through a certain repository. As you might know when you’re a Broadway user, events are often created and sent into the system from a command handler. Let’s say we’re eating a banana, but doing it event sourced. We’d send an EatBanana command which would, inside a CommandHandler, lead to a call eat() on a Banana object:

public function handleEatBanana(EatBanana $command)
{
/** @var Banana $banana */
$banana = $this->bananaRepository->load($command->getBananaId());
    $banana->eat();
    $this->bananaRepository->save($banana);
}

Now, inside this eat() method, a lot of things could happen, but eventually it would lead to this:

public function eat()
{
$this->apply(new BananaEaten($this->bananaId));
}

The Banana object is an EventSourcedAggregateRoot, which means that the apply() method would lead to the event being added to the ‘uncommitted events’ and then saved to the database through the save() command in the previous code fragment.

It’s in this save() method where the magic happens metadata is added to the event. For this to happen, we need to tell the bananaRepository (which is an EventSourcingRepository) about the event stream decorator. This is an object that contains one or more metadata enrichers and can be fed to the repository through its constructor:

class BananaRepository extends EventSourcingRepository
{
public function __construct(EventStoreInterface $eventStore, EventBusInterface $eventBus, EventStreamDecoratorInterface $decorator)
{
parent::__construct($eventStore, $eventBus, Banana::class, new PublicConstructorAggregateFactory(), [$decorator]);
}
}

See how the fifth argument to EventSourcingRepository’s constructor is an array? The object we’re supplying inside that array implements the EventStreamDecoratorInterface. In our case it’s an instance of Broadway’s own MetadataEnrichingEventStreamDecorator.

When you’re using Broadway inside a Symfony project, this decorator is available as a service, so you can inject it directly into your repository. Just add @broadway.metadata_enriching_event_stream_decorator to the service definition of the BananaRepository. And because of tagged services, it’s easy to create your metadata enricher and attach it to the decorator. Before we get to the actual enricher, this is how we’re defining it as a service:

banana_republic.metadata_enricher:
arguments: ['@session', '@banana_republic.infrastructure.doctrine.entity.user_repository']
class: BananaRepublic\Infrastructure\EventStore\MetadataEnricher
tags:
- { name: broadway.metadata_enricher }

See the tag? Broadway will look for all services tagged ‘broadway.metadata_enricher’ and add them to the MetadataEnrichingEventStreamDecorator. And because we’ve added that decorator to our repository, it will be called inside every save() call (look it up and see how the private method decorateForWrite() calls all the event stream decorators) and, in turn, call all the enrichers.

Now we’ve wired up everything to make automatic metadata enrichment happen, it’s time to get back to the initial goal: make sure a user reference (a name, an ID, whatever suits your needs) is added to the metadata of the events.

Modifying the metadata

The class MetadataEnricher above is configured to receive a Symfony session object and access to a user repository. In our case, we’re storing information in the session which refers to more information inside a database, which is why we need those two objects.

The enricher class implements the MetadataEnricherInterface, which implies that it should have a method enrich(), which takes a \Broadway\Domain\Metadata object and also returns one. What you do inside is your business: you can replace the object, or call a merge() method so new data is added to the existing metadata. That’s what we’re doing: we’re using the session and the database access to look up user information, and merging it into the metadata. When we strip that down to its bare minimum, it looks like this:

public function enrich(Metadata $metadata): Metadata
{
return $metadata->merge($metadata->kv('user', $this->getCurrentUser()));
}
private function getCurrentUser(): array
{
$userEntity = $this->repository->find($this->session->get('userid'));
return [
'id' => $userEntity->getId(),
'username' => $userEntity->getUsername(),
];
}

And that’s it. We’ve created a small class that gathers information and adds it to a Metadata object, attached that class as a service to Broadway’s builtin event stream decorator and added the decorator to our event sourcing repositories. Because Broadway has builtin support for metadata enrichment, there’s nothing more we need to do. Now, when a banana is eaten and we look up the BananaEaten event in our database, the ‘metadata’ field looks like this:

{
"class": "Broadway\\Domain\\Metadata",
"payload": {
"user": {
"id": 27,
"username": "Bruce Wayne"
}
}
}

The next step is doing something with this data. While we won’t go into that in too much detail, it comes down to accessing the DomainMessage inside your projector. It looks a bit like this (simplified, of course):

public function applyBananaEaten(BananaEaten $event, DomainMessage $domainMessage)
{
$metadata = $domainMessage->getMetadata()->serialize();
$banana = $this->getReadModel($event->getBananaId());
$banana->setEatenBy($metadata['user']['username']);
$this->repository->save($banana);
}

Now, when you want to display this read model somewhere, you can include a field, column or whatever that mentions that your banana was eaten and by whom.