micro-module/event-store-dbal
A Doctrine DBAL-backed event store for micro-module applications. Persist and replay domain events in a relational database through a simple API suited to event sourcing and CQRS workflows.
1. Validate the package before installing anything.
composer show micro-module/event-store-dbal --all
Check three things in the output: the require block (which doctrine/dbal major it targets), the autoload PSR-4 prefix (you need this to find the entry class), and the PHP constraint. The last release was 2022-05-17, so it almost certainly targets DBAL 2.x and PHP 7.4/8.0. If your app is on DBAL 3/4 or PHP 8.2+, resolve the compatibility question now, not after wiring it into a service container.
2. Read the source before the README.
composer require micro-module/event-store-dbal
ls -R vendor/micro-module/event-store-dbal/src
With zero stars and zero dependents there is effectively no community documentation. The src/ tree is the spec. Look for, in this order:
append/load/loadAll-style methods) — this is your integration surface.*.sql, resources/, docs/, tests/) — this is your migration. Do not hand-write the DDL; copy it from the packaged fixture so column names match what the hydrator expects.3. Install the schema as a migration.
Whatever DDL you find, wrap it in your framework's migration system (Laravel migration, Doctrine migration, Phinx) rather than executing it ad hoc. The package will not manage its own schema lifecycle, and you want the event table to travel with your deploy pipeline.
4. First use case: round-trip one event.
The smallest end-to-end check is: append a single event to a new stream, then load that stream and assert you get the same payload back. Do this in a throwaway test before building a repository around it — it exercises the encoder, the DBAL connection, the transaction handling, and the hydrator in one shot. If that test passes, the package works for your purposes; everything else is layering.
// Illustrative shape — confirm method names against src/ first.
$store->append('order-42', [new OrderPlaced($orderId)], expectedVersion: 0);
$events = $store->load('order-42');
Wrap the store in an aggregate repository. Do not let controllers or handlers touch the event store directly. A thin OrderRepository that maps load() → Order::reconstitute() and save() → append() with the expected version keeps the store behind a seam you can swap out later (which matters a lot given the package's maintenance risk).
Treat the stream version as your concurrency primitive. The whole value of an event store is optimistic concurrency: append with expectedVersion = currentVersion, catch the package's concurrency exception, and either retry the command or surface a conflict to the caller. Find the exception class in src/ and map it to a domain-level ConcurrencyException in your own code so callers never catch a vendor class.
Serialize to JSON, keep the payload dumb. Store the event type as a string discriminator plus a JSON payload, and reconstruct via a type→class map in your own code. This is the single most important layering decision: the package stores bytes, you own the mapping from OrderPlaced to a PHP object. That map is also your migration path if you ever leave this package.
Drive read models with loadAll (or equivalent global-position polling). Projection code should iterate the global event log from a stored checkpoint, apply each event to a read table, and persist the new position in the same transaction as the read-model write. This is the pattern the package exists to enable; if it lacks a global-order read method, that is a disqualifying gap — check before committing.
Use snapshots only after measuring. For long streams, persist an aggregate snapshot every N events and hydrate from snapshot + tail. With DBAL the read cost is usually fine up to a few hundred events per stream; don't build snapshotting on day one.
Bind it in the container once. A single service-provider entry that constructs the DBAL Connection, builds the store, and exposes it as a singleton. Pass the connection in from your app rather than letting the package construct one — you want the event store sharing your app's connection and transaction context, not opening its own.
Pair it with a real outbox if you publish events. If other services consume these events, write to an outbox table in the same transaction as the append and relay from there. Don't publish inline after append().
The maintenance risk is the headline finding. Zero stars, zero dependents, one release in 2022. Budget for the possibility that you are the maintainer from day one. Mitigations, in order of preference: (a) vendor the ~few-hundred lines into your own src/EventStore/ directory, keeping the MIT license header; (b) pin the exact version and add a composer.json conflict rule against DBAL majors you haven't tested; (c) if you must depend on it, wrap every call behind your own interface so replacement is a one-file change.
DBAL major-version drift is the most likely breakage. DBAL 2→3 renamed executeUpdate/executeQuery and changed result fetching; DBAL 3→4 removed more. A 2022-era package will call the old methods. Symptom: Call to undefined method Doctrine\DBAL\.... Fix: check whether your lockfile can hold an older DBAL, or apply a small patch via composer-patches / a fork. Don't upgrade DBAL and hope.
Schema ownership will bite you. The package ships DDL but doesn't migrate it. If a later version changes a column, nothing tells you. Snapshot the DDL into your own migration and treat the package's copy as reference only.
JSON columns are platform-dependent. If the implementation writes to a native json/jsonb column, SQLite in tests behaves differently from MySQL/Postgres. Test against the same engine you deploy on, or verify the encoder stringifies explicitly rather than relying on the driver.
Global ordering gaps are a classic projection bug. Positional reads from an auto-increment column can expose gaps under concurrent transactions — a reader can see position 5 before position 4 commits and then skip 4 forever. If the package's loadAll doesn't handle this, add gap detection or read with a safety lag. Verify the implementation actually orders by a monotonic column, not by timestamp.
Debugging: log the SQL. Set DBAL's SQLLogger (DBAL 3: middleware; DBAL 2: setSQLLogger) and dump the actual statements during your first round-trip test. Because the package has no tests you can trust, the SQL is the ground truth for what's happening.
Extension points to look for, in priority order: a pluggable event serializer/encoder, a configurable table name, and an injectable Connection. If the connection isn't injectable, you cannot participate in your app's transactions — and transactional appends are non-negotiable for an event store. That alone would justify vendoring the code.
How can I help you explore Laravel packages today?