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.
From Aggregate Stream (Recommended)
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.
Always prefer #[FromAggregateStream] when your aggregate class is available. It ensures optimal performance by providing the aggregate type metadata that enables indexed queries on the Event Store.
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:
When using #[FromStream], always provide the aggregateType parameter. Without it, Ecotone cannot use the aggregate type index on the Event Store, resulting in significantly slower event loading — especially on large streams.
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 don't need to match the event name manually in #[EventHandler('ticket.registered')]. As long as the event class has #[NamedEvent], type-hinting the class is enough — Ecotone handles the routing for you.
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:
Using array payloads avoids the cost of deserializing event objects. When rebuilding a projection with thousands of events, this can make a noticeable difference in processing time.
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?