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

Laravel Transactional Events Laravel Package

fntneves/laravel-transactional-events

Laravel Transactional Events delays event dispatching until the surrounding database transaction commits, so listeners never run for rolled-back work. It integrates with Laravel’s event system to keep side effects consistent with persisted data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require fntneves/laravel-transactional-events

The service provider is auto-discovered and swaps the Illuminate\Contracts\Events\Dispatcher binding in the container for a transaction-aware subclass. There is nothing to register in config/app.php on modern Laravel (8+).

Publish the config first

php artisan vendor:publish --tag=transactional-events

(If the tag differs in your version, run php artisan vendor:publish and pick the package from the list — it writes config/transactional-events.php.) Open that file first: it is where you decide whether all events are deferred, only an allow-list, or all-except a deny-list. That single choice defines the blast radius of the package in your app.

First use case — the one that motivates the package

DB::transaction(function () use ($request) {
    $order = Order::create($request->validated());

    event(new OrderPlaced($order)); // NOT fired yet
}); // <- dispatcher flushes OrderPlaced here, only if COMMIT succeeded

Without the package, CreateInvoice, SendOrderConfirmation, NotifyWarehouse etc. fire inside the transaction. If any statement after the dispatch throws and the transaction rolls back, your listeners have already sent mail or charged a card against data that never existed. With the package, those listeners run only after the commit, and are silently dropped on rollback.

Start by converting one write-heavy flow (checkout, registration, any multi-table write with side effects) and leave everything else alone.

Implementation Patterns

Trust the container binding everywhere. event(), Event::dispatch(), dispatch(), and observer/model-event dispatches all resolve the same singleton, so behaviour is uniform. Don't new Dispatcher() anywhere — you'd silently lose the semantics.

Model events and observers are covered for free. created/updated observers fire inside the transaction, so their dispatches are deferred too. This is usually a feature (no "user created" mail for a rolled-back row) but review observers that assume the row is already visible.

Pair with the outbox pattern for anything you cannot lose. Deferred events live in process memory between commit and flush. If the worker is killed in that window (or after commit but before the listener finishes), the event is gone. For money-moving or audit-critical side effects, still write an outbox row inside the transaction and drain it from a queue job — this package then becomes a nicety, not your durability guarantee.

Whitelist rather than global-defer on a mature app. Framework events (Illuminate\Auth\Events\Login, queue events, mail events, broadcasting) get dispatched outside transactions anyway, so they cost nothing — but a strict allow-list keeps surprises out when a new package starts firing events you didn't anticipate. Use the deny list only once you're confident.

Testing pattern. Keep an integration test that asserts the negative case explicitly:

try {
    DB::transaction(function () {
        event(new OrderPlaced($order));
        throw new RuntimeException;
    });
} catch (RuntimeException) {}

Event::assertNotDispatched(OrderPlaced::class); // proves deferral is active

Then use Event::fake() for listener-level assertions as usual.

Layer with queued listeners. For async work, ShouldDispatchAfterCommit on the listener (Laravel 8+) or ->afterCommit() on the queued listener is the native equivalent. This package is valuable precisely where that doesn't help: synchronous listeners, plain closures, and Event::listen-registered handlers.

Gotchas and Tips

No transaction = immediate dispatch. The dispatcher checks the current transaction level; at level 0 the event fires synchronously as before. Code paths that sometimes run inside a transaction and sometimes not will behave differently — that asymmetry bites in tests and in artisan commands.

Rollback drops events silently. No exception, no log line. If a listener was the only thing "verifying" a flow, add an assertion or a debug log in a temporary listener during rollout. This is the single biggest source of "why didn't my email send" confusion.

Event::fake() replaces the dispatcher. The fake is a plain Dispatcher, so the transactional behaviour is bypassed entirely and events are recorded regardless of commit/rollback. Never let a Event::fake() test be your only coverage of rollback semantics — keep the real-dispatcher test above.

Nested transactions and savepoints. Events accumulate until the outermost commit. A rolled-back inner savepoint may or may not discard what was queued inside it depending on how Laravel's transaction manager reports the rollback in your version — verify this specific case with a test if you rely on DB::transaction nesting.

Multiple connections. Deferral is tied to the connection active when the event was dispatched. Dispatching against connection A while the enclosing transaction is on connection B is a classic misread; be explicit with DB::connection('...')->transaction(...) and keep the dispatch inside that closure.

Memory in long transactions. Deferred events are held in memory with their full payload, including Eloquent models. A big import inside one transaction that dispatches per-row will balloon memory and delay side effects until the very end. Batch the transaction or use a listener that only stores IDs.

Post-commit listener exceptions can't roll back anything. A throwing listener at flush time aborts the remaining queued events for that flush. Wrap unreliable side effects (HTTP calls, mail) in try/catch inside the listener, or push them to the queue.

Config is cached. After changing transactional-events.php, run php artisan config:cache (or clear it) and restart queue workers / Octane workers — otherwise long-lived processes keep the old allow/deny lists indefinitely.

Debug tip. To see deferral working, add Event::listen(OrderPlaced::class, fn () => logger('flushed')) and log inside the transaction closure as well; the ordering across the commit boundary is immediately visible.

Check the version constraints before adopting. Last release was March 2025; confirm the composer.json range covers your Laravel major, especially if you're on the newest release, and pin the package accordingly.

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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox