Skip to main content

Dispatch and projections

Subscribers and dispatcher

An EventSubscriberInterface declares the event classes, base classes, or interfaces it handles:

use DomainFlow\EventSourcing\Interface\EventSubscriberInterface;

final class SearchIndexer implements EventSubscriberInterface
{
public static function getSubscribedTo(): array
{
return [ProductCreated::class];
}

public function handle(DomainEventInterface $event): void
{
// Update an external read model or perform another reaction.
}
}

Register subscribers with EventDispatcher and pass the dispatcher to the facade. persist() then dispatches the events that were actually stored:

$dispatcher = new EventDispatcher();
$dispatcher->register(new SearchIndexer());

$facade = new EventSourcingFacade($storage, dispatcher: $dispatcher);

The dispatcher invokes every matching subscriber. It collects subscriber failures and raises SubscriberDispatchException after all matching subscribers have had their turn. A subscriber registered through multiple matching types still receives one call per event.

Projectors

ProjectorInterface extends the subscriber contract with reset(), replay(), supports(), and getName(). ProjectorRegistry collects projectors and registers them with a dispatcher.

Live dispatch and rebuilding are separate concerns: live subscribers receive events as they are persisted, while a rebuild resets a read model and replays the global event stream.

Rebuilding a projection

RebuildProjection uses CatchUpReader, starts at the beginning of the global stream, calls reset(), and hands supported events to replay():

use DomainFlow\EventSourcing\Operation\RebuildProjection;

$result = (new RebuildProjection($storage))($projector);
$result->getEventsReplayed();
$result->getPosition();

For a long-lived or concurrent global reader, use CatchUpReader directly. It keeps a safe resumable position, tolerates a late commit behind the visible frontier, and avoids handing the same aggregate/version identity to the handler twice while it catches up. Persist getSafePosition() after a successful processing cycle.

Inline dispatch versus outbox

Choose one delivery path for a given write. A facade with an inline dispatcher must not also use a storage configured to deliver through an outbox; the facade raises DoubleDeliveryException for that combination when the storage exposes OutboxBackedStorageInterface.