Event Streams and Handlers

PHP Event Sourcing Projection Streams and Event Handlers

The Problem

Your projection needs data from multiple aggregates — orders AND payments — or you want to handle only specific events instead of everything in the stream. How do you control what events reach your projection and how they are routed?

Subscribing to Event Streams

Every Projection needs to declare which Event Streams it reads from. This tells Ecotone where to fetch events when the Projection is triggered.

The most common case — subscribe to all events from a single aggregate type using #[FromAggregateStream]:

#[ProjectionV2('ticket_list')]
#[FromAggregateStream(Ticket::class)]
class TicketListProjection
{
    #[EventHandler]
    public function onTicketRegistered(TicketWasRegistered $event): void
    {
        // handle event
    }
}

#[FromAggregateStream(Ticket::class)] automatically resolves both the stream name and the aggregate type from the Ticket class. This enables Ecotone to use the correct database indexes for fast event loading.

From Multiple Aggregate Streams

When your Read Model combines data from multiple aggregates, use multiple #[FromAggregateStream] attributes:

The Projection will process events from both streams, ordered by when they were stored.

From a Named Stream

In some cases you may need to specify the stream name directly — for example when the aggregate class has been deleted or when targeting a custom stream name. Use #[FromStream] for this:

Event Handler Routing

Ecotone routes events to the correct handler method. You have several options for controlling how this works.

By Type Hint (Default)

The simplest approach — Ecotone routes based on the event class in the method signature:

Named Events

If your events use #[NamedEvent] to decouple the stored event name from the PHP class name:

You can still type-hint your handler with the class — Ecotone automatically resolves the #[NamedEvent] mapping:

You can also subscribe by name explicitly, which is useful when you don't have (or don't want to import) the event class:

Catch-All Handler

To receive every event in the stream regardless of type:

Using Array Payload for Performance

When handling events by name, you can accept the raw array payload instead of a deserialized object. This skips deserialization and can significantly speed up processing — especially useful during backfill or rebuild with large event volumes:

What's Next

Instead of writing raw SQL in your projections, you can use Ecotone's Document Store for automatic serialization and storage — especially useful for rapid prototyping and simpler Read Models.

Last updated

Was this helpful?