Build systems that remember every change. patchlevel gives PHP teams production-ready event sourcing - append-only history, reliable async projections, GDPR-aware operations, and first-class Symfony and Laravel integrations.
Four moving parts. Events are the truth, the store is the log, the subscription engine fans events out, projections build read models. That's the whole picture.
Pure PHP classes. No base class, no magic - just immutable data with one attribute that gives it a name in your store.
#[Event('hotel.created')] final class HotelCreated { public function __construct( public Uuid $hotelId, public string $name, ) {} }
Aggregates capture changes as events and defines business rules. Call recordThat - patchlevel handles persistence, replay, and ordering for you.
#[Aggregate('hotel')] final class Hotel extends BasicAggregateRoot { #[Id] private Uuid $hotelId; public static function create(Uuid $hotelId, string $name): self { $self = new self(); $self->recordThat(new HotelCreated($hotelId, $name)); return $self; } #[Apply] public function onHotelCreated(HotelCreated $event): void { $this->hotelId = $event->hotelId; } }
Save the aggregate. Load it back later - its full event history is replayed automatically to reconstruct state.
$hotelId = Uuid::generate(); $hotel = Hotel::create($hotelId, 'Patchlevel HQ'); // save the aggregate $repository = $repositoryManager->get(Hotel::class); $repository->save($hotel); // load it back - events are replayed automatically $hotel = $repository->load($hotelId);
Subscribers react to events asynchronously to build read models, fire side effects, or feed external systems - versioned, gap-detected, retried on failure.
#[Projector('hotels')] final class HotelProjection { #[Subscribe(HotelCreated::class)] public function onHotelCreated(HotelCreated $event): void { $this->db->insert('hotels', [ 'id' => $event->hotelId->toString(), 'name' => $event->name, ]); } }
The two questions we hear most often about event sourcing. Here's our honest take on each.
The Subscription Engine powers reliable event processing for projections, processors, and asynchronous workflows. It manages subscriber execution, replay, and recovery, ensuring events are processed consistently and at scale.
Boot, replay, catch up, pause, reactivate, or retire subscribers with a fully managed lifecycle.
Gap detection, retry strategies, and failure handling ensure events are processed consistently, even under load.
Rebuild projections, rerun processors, or migrate data directly from the event stream.
Deploy new subscriber versions alongside existing ones and migrate safely using blue-green deployment strategies.
Process events in batches for significantly faster catch-ups and rebuilds.
Run subscriptions from the beginning, from now, or as a one-time execution.
Process million-event projections without losing your weekend.
#[Processor('new-guest-mail')] final class NewGuestProcessor { #[Subscribe(GuestCheckedIn::class)] public function onGuestCheckedIn(GuestCheckedIn $event): void { $this->mailer->send('new-guest.html', [...]); } #[OnFailed] public function onFailed(Throwable $e): void { $this->deadLetterQueue->push($e); } }
Mark fields with attributes. The encryption keys are stored in a different store. Forget the encryption key. That user's data is unreadable forever - event store untouched, history intact, audit trail preserved.
#[Event('customer.registered')] final class CustomerRegistered { public function __construct( #[SubjectId] public Uuid $id, #[PersonalData(fallback: 'anon@patchlevel.dev')] public string $email, #[PersonalData] public string $name, ) {} } // later - right-to-be-forgotten $cipherKeyStore->remove($customerId);
patchlevel/hydrator - encryption layer adds no measurable hot-path overhead.Real-world business rules often span multiple aggregates. Two complementary patterns let you model shared consistency boundaries - one production-ready today, one shaping the future.
Split a complex aggregate into focused roots that share a single event stream via the #[Stream] attribute. Each root stays small and intentional, while related entities remain transactionally consistent.
#[Aggregate('order')] final class Order { // ... } #[Aggregate('order-payment')] #[Stream(Order::class)] final class OrderPayment { // ... }
Make consistent cross-stream decisions without loading any aggregate at all. Tag events with #[EventTag] and build minimal, purpose-built state from just the events that matter - with an optimistic append condition to keep it race-free.
#[Event('hotel.guest_checked_in')] final readonly class GuestIsCheckedIn { public function __construct( #[EventTag(prefix: 'hotel')] public Uuid $hotelId, #[EventTag(prefix: 'guest')] public string $guestName, ) {} }
Everything you need to manage complex event streams at scale, in one cohesive library.
First-class "Given, When, Then" with in-memory stores. Lightning-fast unit tests for business logic.
$this->given([new HotelCreated(...)]); $this->when(new CheckInGuest(...)); $this->then([new GuestCheckedIn(...)]);
Built-in command and query handling with attribute-driven handlers in your aggregates or projections. Fully compatible with Symfony Messenger.
Stop wasting time on boilerplate. Our integrations handle the plumbing so you can focus on your business logic.
patchlevel/event-sourcing-bundle
Drop the bundle in and every repository, store, and subscriber wires itself into the container. Messenger, Doctrine, the profiler - all integrated.
$ composer require patchlevel/event-sourcing-bundle
patchlevel/laravel-event-sourcing
A native Laravel feel. The service provider auto-registers everything, Artisan picks up the commands, and facades give you easy access.
$ composer require patchlevel/laravel-event-sourcing
The short version of what developers ask us before they start event sourcing with patchlevel.
Yes - and that's our promise: it will never cost anything. The event sourcing library and all integrations are open source under the MIT license, with no paid tiers and no feature gates - not today, not in the future. Every feature you see here, from snapshots to subscriptions, is and stays free. If you want hands-on help, the patchlevel team also offers professional consulting and training.
Event sourcing and CQRS are a natural fit, and the library supports both: aggregates handle your writes, while subscriptions build dedicated read models (projections) optimized for your queries. You can wire in any command bus - like Symfony Messenger - or none at all. CQRS is supported, not forced.
No. The event store runs on the relational database you already operate - PostgreSQL, MariaDB, MySQL, or SQLite - powered by the battle-tested Doctrine DBAL. No extra infrastructure to deploy, back up, or monitor.
No. You can adopt event sourcing per aggregate, exactly where the business value lives - your orders, bookings, or payments - and keep simple CRUD for the rest. The library coexists peacefully with Doctrine ORM, Eloquent, and existing code.
Software evolves, and so do events. Upcasters transform old event payloads on the fly when they are loaded, so you never have to migrate the event store itself. Your history stays immutable while your code moves forward.
Very. We are 100% committed to semantic versioning: no breaking changes within a major version, ever. Deprecations are announced ahead of time and every upgrade path is documented, so updating stays painless. And you are not on your own - we actively maintain the library and provide support on GitHub, with professional support available if you need more.
No problem. The core library is plain PHP with no framework dependency, so it works in any project - Slim, Laminas, CodeIgniter, or no framework at all. If you use Symfony or Laravel, official integrations give you a head start: a Symfony bundle with autowiring, Messenger, and Doctrine migrations, and a Laravel package with auto-discovery and Artisan commands.
Start with the documentation - it covers everything from your first aggregate to advanced topics like snapshots and subscriptions. For questions and bug reports, the team is active on GitHub. And if you need deeper support, patchlevel offers consulting, code reviews, and workshops.
Get started in five minutes. The docs walk you through your first aggregate, event, and projection.