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

Domain Events Bundle Laravel Package

headsnet/domain-events-bundle

Symfony bundle for domain events: define and dispatch events from your domain, register subscribers, and defer delivery until after Doctrine transactions commit, keeping business logic decoupled from infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a Symfony bundle: it ships domain-event primitives (an event recorder on aggregate roots, a dispatcher, and tag/attribute-based listener registration). Two realistic entry points from a Laravel codebase:

  • Greenfield Laravel app: install the Laravel-flavoured sibling (headsnet/laravel-domain-events) instead of the bundle. The concepts (aggregate root, event recorder, listener registration) are identical, and the v0.4.5 transaction behaviour described below applies to the same underlying model.
  • Shared/ported code: if you are wiring the bundle itself into Laravel (via a service-provider bridge), composer require headsnet/domain-events-bundle, register the bundle/provider, and confirm the dispatcher is resolvable from the container before writing a single listener.

Where to look first, in this order:

  1. The AggregateRoot / event-recorder trait — this is where record(new SomethingHappened(...)) lives.
  2. The dispatcher service and the listener registration mechanism (tags/attributes) — confirm your listeners are actually discovered before debugging anything else.
  3. The transaction guard added in v0.4.5 — this is the one thing that changes when listeners fire, so read it before you design your first use case.

First use case — and make it a two-part experiment, because it settles the single most important question about v0.4.5:

// 1) outside a transaction
$order = Order::place($command);
$this->orders->save($order);
$this->events->release($order);   // observed: fires immediately?

// 2) inside a transaction
DB::transaction(function () use ($order) {
    $this->events->release($order);   // observed: fires now, at commit, or never?
});

Observe both. If the listener fires only after commit, the guard defers. If it never fires, the guard suppresses and you must move the release outside the transaction. Do not assume which one you have — the release note ("prevent event publishing during active transactions") is compatible with either implementation, and your entire integration strategy depends on the answer.

Implementation Patterns

Record on the aggregate, release at the boundary. Entities only record events; the application layer releases them after persistence. This keeps entities container-free and makes the transaction window explicit rather than implicit.

Prefer dispatching after the transaction commits. Given v0.4.5, the safest default shape is:

$order = DB::transaction(function () use ($command) {
    $order = Order::place($command);   // records OrderPlaced
    $this->orders->save($order);
    return $order;
});

$this->events->release($order);        // outside the transaction: deterministic

This works identically whether the guard defers or suppresses, and it removes any dependency on how the package implements the guard. Reach for release-inside-the-transaction only once you've confirmed the semantics with the experiment above.

If you do release inside a transaction, design for "later". Treat every listener as running after the unit of work has closed. It must not read rows written later in the same transaction, and it must not depend on the request's in-memory state.

Keep listeners idempotent and side-effect-only. Whatever the guard does, delivery can still be retried or duplicated (queue workers, commit retries). Key each listener's work on a stable event ID so replays are safe.

Fan out, don't build a god-listener. Register several small listeners per event (notification, projection, integration sync) so one failure doesn't take down the others.

Bridge to Laravel's dispatcher by decoration. Wrap the package dispatcher in a thin adapter that calls Event::dispatch(). Existing Event::listen() and ShouldQueue listeners keep working, while the aggregates themselves stay framework-agnostic. The decorator is also the right seam for logging and metrics.

Testing pattern. Unit-test the aggregate by asserting releaseEvents() returns the expected objects. Integration-test the transaction behaviour with two tests: one asserting the listener does not run synchronously inside an open transaction, and one asserting it does run after commit (or is intentionally dropped) — pin the actual behaviour so a future patch bump can't silently change it.

Gotchas and Tips

v0.4.5 is a behavioural change shipped in a patch release. The changelog line is one sentence; the impact is not. Any listener that previously fired mid-transaction — writing to a second connection, pushing to a search index, reading the just-written row through a different PDO handle — must be re-audited. Some of those will now see missing data, and some will not run at all.

Rollbacks and suppression. If the guard defers, a rolled-back transaction publishes nothing. If it suppresses, nothing was ever queued. Either way, "audit every attempt including failures" needs a separate, non-transactional path — do not try to get it from domain events.

Nested transactions and savepoints. Releasing in an inner transaction while an outer one is open means the event waits for (or is blocked by) the outermost scope. Keep nesting shallow in application code; DB::transaction() in a listener is an easy way to accidentally nest.

Long-running transactions. A batch job holding one transaction open across thousands of records is the worst case: either events pile up in memory and burst at commit, or they're silently dropped. Chunk the work into per-chunk transactions and release per chunk.

Ordering is no longer dispatch-ordered. Once publishing is coupled to transaction boundaries, two events recorded in sequence across different transactions can publish in commit order. Never encode ordering assumptions in a listener; carry an explicit sequence or version field on the event if you need it.

Debugging "the listener silently doesn't fire". First suspect a transaction. Check DB::transactionLevel() > 0 at release time, and temporarily log inside the package's publish/guard hook to see which events were actually handed to the dispatcher. Only after you've ruled out the transaction should you go looking at listener registration/discovery.

Extension points. The dispatcher is decoratable — that's the cleanest place for logging, metrics, and the Laravel Event bridge. If the transaction awareness sits behind a transaction-manager abstraction, that's the seam for non-default connections. Don't patch the vendor code to change guard behaviour; decorate.

Version pinning. Pin ^0.4.5 and read the changelog before every bump. The 0.x line has shipped behavioural changes in patch releases, and this one is a case in point.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource