Skip to content

Events

Events are used to describe things that happened in the application. Since the events already happened, they are also immutable. In event sourcing, these are used to save and rebuild the current state. You can also listen on events to react and perform different actions.

An event has a name and additional information called payload. Such an event can be represented as any class. It is important that the payload can be serialized as JSON at the end. Later it will be explained how to ensure it for all values.

To register an event you have to set the Event attribute over the class, otherwise it will not be recognized as an event. There you also have to give the event a name.

use Patchlevel\EventSourcing\Attribute\Event;

#[Event(name: 'profile.created')]
final class ProfileCreated
{
    public function __construct(
        public readonly string $profileId,
        public readonly string $name,
    ) {
    }
}

Warning

The payload must be serializable and unserializable as json.

Tip

An event should be named in the past because it has already happened.

Best practice is to prefix the event names with the aggregate name, lowercase everything, and replace spaces with underscores. Here are some examples:

  • profile.created
  • profile.name_changed
  • hotel.guest_checked_out

Alias

You also have the option to set aliases for the events. This can be useful when you want to rename events but still need to process the old ones.

use Patchlevel\EventSourcing\Attribute\Event;

#[Event(name: 'profile.registered', aliases: ['profile.created'])]
final class ProfileRegistered
{
}
When saving, the name will always be used. However, when loading, aliases will also be taken into account.

Note

In the database, the name of the event is always stored, allowing the class to be renamed without encountering any issues.

Tip

If you want to make significant changes to an event, you can take a look at the Upcaster.

Serializer

So that the events can be saved in the database, they must be serialized and deserialized. That's what the serializer is for. The library comes with a DefaultEventSerializer that can be given further instructions using attributes.

use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;

$serializer = DefaultEventSerializer::createFromPaths(['src/Domain']);
The serializer needs the path information where the event classes are located so that it can instantiate the correct classes. Internally, an EventRegistry is used, which will be described later.

Normalizer

Sometimes you also want to add more complex data as a payload. For example DateTime or value objects. You can do that too. However, you must define a normalizer for this so that the library knows how to write this data to the database and load it again.

use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Event;
use Patchlevel\EventSourcing\Serializer\Normalizer\IdNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

#[Event('profile.created')]
final class ProfileCreated
{
    public function __construct(
        #[IdNormalizer]
        public readonly Uuid $id,
        #[NameNormalizer]
        public readonly Name $name,
        #[DateTimeImmutableNormalizer]
        public readonly DateTimeImmutable $createdAt,
    ) {
    }
}

Tip

Built-in normalizers like IdNormalizer and DateTimeImmutableNormalizer can be inferred from the type hint and so you don't have to specify them. If you want to configure the Normalizer, you still have to do it.

Note

You can find out more about normalizer here.

Event Registry

The library needs to know about all events so that the correct event class is used for the serialization and deserialization of an event. There is an EventRegistry for this purpose. The registry is a simple hashmap between event name and event class.

use Patchlevel\EventSourcing\Metadata\Event\EventRegistry;

$eventRegistry = new EventRegistry([
    'profile.created' => ProfileCreated::class,
]);
So that you don't have to create it by hand, you can use a factory. By default, the AttributeEventRegistryFactory is used. There, with the help of paths, all classes with the attribute Event are searched for and the EventRegistry is built up.

use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory;

$eventRegistry = (new AttributeEventRegistryFactory())->create([/* paths... */]);

Learn more