Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Event Sourcing Laravel Package

patchlevel/event-sourcing

A fully featured, developer-friendly event sourcing library for PHP built on Doctrine DBAL. Includes automatic snapshots, stream splitting, versioned subscriptions, crypto-shredding for personal data, event upcasting, and CLI commands.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require patchlevel/event-sourcing
php artisan vendor:publish --tag="event-sourcing-config"
php artisan vendor:publish --tag="event-sourcing-migrations"
php artisan migrate

The package is framework-agnostic at its core with a Laravel service provider layered on top. The config file it publishes (config/event-sourcing.php) is the single most important file to read first — it controls the event store driver, serializer, snapshot store, subscription/queue behaviour, and the header/message serialization settings that the 3.22 line touches.

Where to look first

  1. config/event-sourcing.php — drivers, serializers, snapshot config, metadata factory configuration.
  2. The AggregateRoot base class and one or two example aggregates in the docs — this is the mental model everything else hangs off.
  3. The migration stubs — they reveal the actual shape of the stored_events / snapshot / subscription tables, which is the fastest way to understand what the package persists.

First use case

Pick a single, well-bounded aggregate — a Cart, Order, or Account — and implement exactly one command path: record a Created event, persist via the repository, and project it into one read-model table with one projector. Do this before touching reactors, snapshots, or subscriptions. The value of event sourcing only becomes obvious once you have replayed that one projector from scratch, so build that loop early.

Implementation Patterns

Aggregate-centric command flow

Commands resolve an aggregate from the repository, call a domain method that records events, and the repository persists them. Keep apply* methods pure state mutation — no I/O, no dispatch, no clocks — because they run both on the write path and during reconstitution from the stream. Put invariants in the recording method (throw before recording), not in apply.

Projectors vs reactors

  • Projectors rebuild read models. Treat them as replayable and idempotent: use upsert/updateOrInsert, never increment or insert with auto-increment assumptions. Design them so running the projector twice over the same stream produces the same table state.
  • Reactors handle side effects (mail, HTTP calls, queueing). Assume they can run more than once; make them idempotent or keyed by event id.
  • Register handlers with the on(EventClass::class) style so the mapping is explicit and greppable; avoid magic string matching.

Subscriptions for async work

Use a subscription per read model / consumer, with the package's checkpointing table, rather than ad-hoc "replay everything" scripts. Typical day-to-day flow:

php artisan event-sourcing:subscription <name>   # run the consumer
# plus a catch-up/replay command provided by the package

Run subscriptions as supervised workers; they are stateful (they track position), so don't treat them like stateless queue workers you can kill mid-batch without checking the checkpoint behaviour of your configured store.

Snapshots

Add snapshots only for aggregates whose streams grow unbounded (thousands of events). Configure the snapshot store and a threshold in config, and implement the aggregate's snapshot state deliberately — snapshotting a partially-hydrated aggregate is a common source of subtle bugs. Snapshots are an optimisation, never a source of truth.

Headers / metadata and header aliases (3.22.0)

Headers (message metadata) are stored alongside events and must be deserialized back into header objects when the stream is read. The 3.22.0 change adds support for header aliases, i.e. you can map a stored alias string to a header class, decoupling persisted data from your PHP class names.

Day-to-day pattern this enables:

  • Rename-safe metadata. When you move or rename a header class (e.g. App\Headers\UserId → App\Domain\Headers\ActorId), register the old name as an alias so existing stored events still deserialize. This removes the long-standing sharp edge where renaming a header class silently broke replay of historical events.
  • Stable external contracts. If you ingest events from another service or an older version of your own app, register aliases for the wire-format names those producers use, and keep your internal class names free to evolve.
  • Backwards-compatible migrations. Register both the current and previous alias for a header class before deploying the rename, so a rolling deploy where old and new code coexist can still read each other's events.

Practical advice: treat the alias map as part of your persistence contract — put it under version control, review it in PRs like a migration, and add an alias before you rename, never after. Keep one canonical alias per header class for new writes and reserve the extra aliases for reads of historical data.

Metadata factories

If you use (or write) metadata factories to attach headers to outgoing messages, keep them cheap and correct under caching. 3.22.1 fixes the cache keys used by the package's metadata factories (PR #930), so if you are on 3.22.0 and have caching enabled for metadata factories, upgrade before relying on cached metadata. When implementing a custom factory, make sure the cache key you hand back is derived from everything that affects the produced metadata (event/aggregate identity, user/correlation context) — two different messages must never share a key.

Testing

  • Unit-test aggregates directly: record a command, assert the recorded events and the resulting state. No DB needed.
  • Use the package's in-memory/fake event store (or a test DB with the real store) for projector tests, and assert projector output by replaying a known event list — this catches non-idempotency early.
  • Add a "rename test" whenever you touch header or event class names: serialize an event, deserialize it, assert the header object round-trips.
  • Add a metadata factory test that asserts two distinct messages produce distinct metadata (and that cached and uncached paths agree) — this is exactly the class of bug 3.22.1 fixed.

Gotchas and Tips

Header alias pitfalls (3.22.0)

  • An unregistered alias is the failure mode to watch for: historical events will fail to deserialize, or headers will come back unresolved depending on your serializer configuration. If a replay suddenly throws after a class rename, check the alias map first.
  • Avoid registering two aliases that collide with an existing header class name; ambiguous mappings make deserialization order-dependent. Namespace your aliases (e.g. app.user_id) rather than relying on bare class basenames.
  • Aliases are for headers. Event class names are typically handled by a separate event-class map/upcasting mechanism — don't conflate the two when planning a rename.

Metadata factory cache keys (fixed in 3.22.1)

  • On 3.22.0, metadata factories could cache under the wrong key. Symptoms are subtle rather than loud: headers that are correct on the first message but stale/wrong on subsequent ones, or metadata bleeding between events within a request or worker lifetime. If you see correlation/user headers that "stick" to the wrong event, upgrade to 3.22.1 first before hunting in your domain code.
  • This is a patch-level fix: no API change, nothing to adopt. Just bump the constraint and run your test suite.
  • If you maintain custom metadata factories, treat their cache key as part of the contract and test collision cases explicitly.

Class renames in general

Both event classes and header classes are embedded in stored data. Any rename is a data migration. The safe sequence is always: deploy alias/map → deploy new class → backfill/upcast at leisure → remove old alias only when nothing historical references it.

Idempotency and replay

  • Projectors that mutate counters, send notifications, or rely on now() will diverge on replay. Keep time and randomness out of apply methods; derive timestamps from the event's recorded metadata.
  • Replaying into a table with unique constraints will fail unless the projector upserts.
  • Long replays: batch reads and disable reactive side effects (reactors) during replays; only projectors should run.

Transactions and consistency

Recording events and writing the read model are not atomic unless you deliberately make them so. Prefer async projectors with subscriptions for anything that can tolerate lag, and synchronous projectors only where a stale read model is genuinely unacceptable. Watch out for writing to the read model inside the same transaction as the event store — it couples your write latency to projection cost.

Config quirks

  • Serializer and driver choices are baked into existing stored data. Changing the serializer on a live store without migrating data is a one-way ticket to unreadable events.
  • Snapshot config changes (threshold, store) are safe; snapshot format changes are not — version your snapshot payloads.
  • Queue configuration for reactors/subscriptions is separate from your app's default queue; verify which connection is used before assuming workers will pick jobs up.
  • Metadata factory caching is configured/implemented separately from the event store cache — check which cache store it resolves to, especially in tests and queue workers.

Extension points worth knowing

  • Custom header serializers/deserializers — the natural place to handle special header types, and where alias resolution plugs in.
  • Metadata factories (and their cache keys) — the cleanest way to attach correlation/causation/user headers to every outgoing event without touching domain code.
  • Message decorators — the cleanest way to attach correlation/causation/user headers to every outgoing event without touching domain code.
  • Custom event store / snapshot store implementations behind the config driver keys.
  • Upcasters for evolving event payloads across versions (distinct from header aliases).

Upgrade guidance for 3.22.x

  • 3.22.1 is a one-PR patch release fixing metadata factory cache keys. No breaking changes, no deprecations, nothing to configure. Upgrade if you are on 3.22.0 and use metadata factories with caching; otherwise it is a routine low-risk bump.
  • 3.22.0 was a minor feature release (header aliases) with no reported breaking changes. It is additive: existing behaviour is unchanged unless you register aliases. Adopt the alias mechanism opportunistically the next time you need to rename a header class — retrofitting aliases onto already-stored events works, but is far easier if the alias is in place before the rename ships.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console