neos/event-sourcing-symfony-bridge
Symfony bridge for Neos Event Sourcing, wiring event stores, projections, and subscribers into the framework. Provides dependency injection, console commands, and configuration to run event-sourced applications smoothly on Symfony.
This bridge wires the standalone neos/event-sourcing library into a Symfony app: it registers the event store, auto-wires listeners and projections, and exposes CLI commands for replay. It is thin glue, not a framework — you still model events, listeners, and read models yourself.
Install and register the bundle
composer require neos/event-sourcing-symfony-bridge
Enable it in config/bundles.php (namespace is Neos\EventSourcing\SymfonyBridge\...). Run bin/console config:dump-reference for the bundle and bin/console debug:container --tags to see exactly which tags it registers — the package has few stars and no recent releases, so verify names against the installed version rather than trusting blog posts.
Configure the event store connection. You need a Doctrine DBAL connection and a table (often created via a migration or the bundle's schema command, if present). Point the bridge at your connection and table name in config/packages/*.yaml; use a separate connection if you want event writes isolated from your app's main transactional workload.
Look first at three things: the bundle's DependencyInjection/ extension (what config keys and tags exist), its Command/ directory (replay/reset/setup commands), and the upstream neos/event-sourcing interfaces (EventStore, EventListenerInterface, ProjectionInterface). The bridge only makes sense once you understand those contracts.
First use case: a rebuildable read model. Define one event (UserRegistered), one projection that writes to a users read table, append the event from a controller or command handler, and call the projection synchronously. Then run the replay command against that projection to prove you can rebuild it from scratch. That loop — append → project → replay — is the entire mental model of this package; everything else is scale and plumbing.
Append events from your application services, not from entities. The typical flow is: your command handler validates input, constructs a plain event DTO, and calls the event store. The bridge's store service is the single write point; keep it out of Doctrine entities and out of controllers so you can later swap synchronous publishing for async.
Two consumer styles, both tag-based.
EventListenerInterface) run on publish and are for side effects — sending mail, dispatching to a queue, updating a small lookup. They are not rebuildable in practice; never put your primary read model here.ProjectionInterface) are the rebuildable read models. They get the full event stream on replay. Put every table you would want to regenerate here, and give each projection its own reset() that truncates only its own tables.Register both via the bundle's service tags rather than by service ID — autoconfiguration won't find them, since they implement upstream interfaces the framework doesn't know about.
Synchronous first, async later. Get the projector working inline, then introduce an async publisher (Symfony Messenger or the library's async decorator) once write latency matters. Because projections are replayable, switching from sync to async is a config change plus a replay — design for that from day one and you avoid rewrites.
Replay as a routine operation, not an incident response. Before each deploy that changes a projection's schema, run reset + replay in staging, measure duration, and record the event-store size. In production, replay into a shadow table and swap when the CLI command is available; otherwise schedule a maintenance window. The bridge's replay commands are the main operational tool you'll use week to day.
Event versioning. Treat the event class name + payload as a contract. When a field must change, add a new event version and an upcaster or a new listener branch; do not mutate the old event class. The store serializes to JSON by default, so add a discriminator (eventVersion or a v2 class) and keep both readable.
Integration tips.
reset(); that name is what the replay command targets.EventPublisher service) so the rest of the app never touches the bridge directly — that is your seam if the package is abandoned.neos/event-sourcing version locked alongside it — the bridge is version-coupled and has historically lagged DBAL major versions.composer update fights you on DBAL, that is why. Test on a fresh install before touching a legacy one.debug:container --tag=<tag> — this is the single most common "why isn't my projection running" cause.reset then replay, and make every projection upsert-safe anyway.DateTime and value objects need an explicit normalizer or you get inconsistent payloads between write and read. Avoid closures, resources, and circular references.retry_strategy and a failure transport. A projection that throws on a bad event will loop forever otherwise; add a dead-letter path and a command to skip or fix the offset.EventStore implementation (e.g., Postgres-native) by aliasing the interface, and implement EventTypeResolver if you want class FQCNs decoupled from stored type strings. These are the three seams that let you outlive the package itself.How can I help you explore Laravel packages today?