The official Symfony bundle for patchlevel/event-sourcing. Autowired repositories, Symfony Messenger as command and query bus, Doctrine migrations for the event store, and console commands for the whole projection lifecycle - event sourcing that feels like Symfony.
$ composer require patchlevel/event-sourcing-bundle # the Flex recipe sets up the configuration $ bin/console event-sourcing:database:create $ bin/console event-sourcing:schema:create
Define events and aggregates as plain PHP, then let the bundle do the wiring: repositories are autowired, subscribers are autoconfigured, and every service is a first-class citizen of the container.
Pure PHP classes with a single attribute - no base class, no framework coupling. Your domain model stays clean and testable.
#[Event('hotel.guest_is_checked_in')] final class GuestIsCheckedIn { public function __construct( public readonly string $guestName, ) {} }
Business rules live in the aggregate. Record events with recordThat - the library replays them to rebuild state, in Symfony and everywhere else.
#[Aggregate('hotel')] final class Hotel extends BasicAggregateRoot { #[Id] private Uuid $id; /** @var list<string> */ private array $guests = []; public function checkIn(string $guestName): void { if (in_array($guestName, $this->guests, true)) { throw new GuestAlreadyCheckedIn($guestName); } $this->recordThat(new GuestIsCheckedIn($guestName)); } #[Apply] protected function applyGuestIsCheckedIn(GuestIsCheckedIn $event): void { $this->guests[] = $event->guestName; } }
Type-hint Repository<Hotel> in any controller or service and the bundle injects the right repository. Load, call business methods, save - persistence and replay are handled for you.
#[AsController] final class HotelController { /** @param Repository<Hotel> $hotelRepository */ public function __construct( private readonly Repository $hotelRepository, ) {} #[Route('/hotel/{hotelId}/check-in', methods: ['POST'])] public function checkIn(Uuid $hotelId, Request $request): JsonResponse { $hotel = $this->hotelRepository->load($hotelId); $hotel->checkIn($request->getPayload()->get('name')); $this->hotelRepository->save($hotel); return new JsonResponse(); } }
Projectors and processors are regular Symfony services - inject the Mailer, a DBAL connection, or anything else. Autoconfiguration registers them with the subscription engine.
#[Processor('admin_emails')] final class SendCheckInEmailProcessor { public function __construct( private readonly MailerInterface $mailer, ) {} #[Subscribe(GuestIsCheckedIn::class)] public function onGuestIsCheckedIn(GuestIsCheckedIn $event): void { $this->mailer->send( (new Email()) ->to('hq@patchlevel.de') ->subject('Guest is checked in') ->text(sprintf('"%s" checked in', $event->guestName)), ); } }
Not a generic library with a thin adapter. The bundle plugs event sourcing into the tools you already use every day.
Point the bundle at your Messenger buses and aggregates become command handlers. No handler classes full of load-call-save boilerplate - dispatch a command and the bundle loads the aggregate, invokes the method, and saves the recorded events.
Mark aggregate methods with #[Handle] - the bundle registers them as Messenger handlers, including aggregate loading and saving.
Answer queries from services with the #[Answer] attribute and dispatch them through a dedicated query bus.
Optimistic-lock conflicts like AggregateOutdated are retried automatically with a configurable strategy.
framework: messenger: default_bus: command.bus buses: command.bus: ~ query.bus: ~
patchlevel_event_sourcing: command_bus: service: command.bus query_bus: service: query.bus
The integration ships everything from patchlevel/event-sourcing - production features you would otherwise build yourself.
Managed lifecycle for projections and processors: replay, rebuild, retry, gap detection, and blue-green versioning.
Mark fields as personal data, store keys separately, and make a subject unreadable forever - without rewriting history.
Automatic, cache-backed snapshots keep replays fast for long-lived aggregates.
Evolve old events on the fly, or rewrite them permanently when you retire legacy formats.
Archive past lifecycle phases and load only the active slice - full history preserved.
Given-When-Then testing with in-memory stores for lightning-fast unit tests of business logic.
Everything you need to know about event sourcing in Symfony with patchlevel.
Run composer require patchlevel/event-sourcing-bundle. The Symfony Flex recipe registers the bundle and creates the default configuration, so you can define your first aggregate right away. The event store schema is created with bin/console event-sourcing:schema:create or via Doctrine migrations.
Yes. You can use Symfony Messenger as command bus and query bus. Aggregates and services handle commands via the #[Handle] attribute, queries via #[Answer] - the bundle registers the handlers with Messenger automatically.
The event store is built on Doctrine DBAL and supports PostgreSQL, MySQL, MariaDB, and SQLite out of the box. Custom stores for other technologies can be implemented against clean interfaces.
Yes. Event-sourced aggregates and Doctrine entities coexist in the same application. Projections often write read models as plain database tables or Doctrine entities, and the bundle manages the event store schema with Doctrine migrations.
Projections are managed by the subscription engine. Console commands like bin/console event-sourcing:subscription:boot and event-sourcing:subscription:run let you set up, replay, and rebuild projections directly from the event stream - including blue-green deployments of new projection versions.
Yes. The patchlevel libraries have over 300,000 Packagist installs, are MIT licensed, fully typed for PHPStan and Psalm, and power production systems with GDPR-compliant crypto-shredding, snapshots, and a managed subscription lifecycle.
Install the bundle and build your first aggregate, event, and projection in minutes - the getting-started guide walks you through it.