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 Laravel Package

complex-heart/domain-events

Domain events for Laravel: record and dispatch events directly from your domain models, then handle them with listeners to decouple business logic. Keeps entities clean, testable, and framework-light.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and orient yourself in the source, not the README. The package is small enough that src/ is the documentation:

composer require complex-heart/domain-events

Then open these files first, in order:

  1. DomainEvent — the contract every event implements. Note what it requires: typically an event name/type and a payload/attributes accessor.
  2. RecordsEvents — the trait you put on aggregates/entities. This is where recordEvent(), releaseEvents(), and the internal collection live.
  3. EventBus — the one-method abstraction (dispatch(...)) that keeps your domain free of Laravel's dispatcher.
  4. EventCollection / message-related classes — how recorded events are grouped and iterated.

First use case: record on the aggregate, release at the application boundary. The canonical flow is:

use ComplexHeart\Domain\Events\RecordsEvents;

final class Subscription
{
    use RecordsEvents;

    public function cancel(): void
    {
        // ... mutate state ...
        $this->recordEvent(new SubscriptionCancelled($this->id, now()));
    }
}
// In a command handler / application service
$subscription->cancel();
$repository->save($subscription);

foreach ($subscription->releaseEvents() as $event) {
    $this->bus->dispatch($event);
}

If you only take one thing from this package, it's that split: the domain records, the application layer releases and dispatches. Do not dispatch from inside the entity.

Implementation Patterns

Bind the EventBus to Laravel's dispatcher once, in a service provider. This is the single integration point that makes the package feel native:

$this->app->singleton(EventBus::class, fn ($app) => new LaravelEventBus($app['events']));
final class LaravelEventBus implements EventBus
{
    public function __construct(private Dispatcher $dispatcher) {}

    public function dispatch(object $event): void
    {
        $this->dispatcher->dispatch($event);
    }
}

From there, your domain events are Laravel events — listeners, ShouldQueue, Event::fake() in tests all work with zero extra glue. You get the package's aggregate-side ergonomics plus the framework's listener infrastructure.

Wrap release-and-dispatch in a transaction-aware boundary. The most common real bug is dispatching before commit, so listeners query stale data. In Laravel 8+:

DB::transaction(function () use ($subscription) {
    $subscription->cancel();
    $this->repository->save($subscription);

    DB::afterCommit(fn () => $this->releaseAndDispatch($subscription));
});

Centralize releaseAndDispatch() in a small trait or base command handler; every handler that mutates an aggregate should call the same method.

Use it as the event-recording half of an outbox. Because releaseEvents() hands you a plain iterable, it composes cleanly with an outbox table: persist serialized events in the same transaction as the aggregate, then a job drains and dispatches them. You keep the package for the in-memory recording and swap the dispatch step for the queue.

Test at the aggregate level without any bus. This is where the package earns its keep day-to-day:

$subscription->cancel();

$events = $subscription->releaseEvents();

$this->assertCount(1, $events);
$this->assertInstanceOf(SubscriptionCancelled::class, $events[0]);

Unit tests for aggregates stop needing Laravel at all. Keep a separate, thinner set of integration tests that assert the bus actually reaches your listeners.

Model event payloads as flat, serializable value objects. Scalar IDs, strings, timestamps, enums. Anything queued (ShouldQueue listeners) gets serialized; Carbon, entities, or closures in the payload will bite you.

Gotchas and Tips

releaseEvents() is destructive. It drains the collection. If you call it in two places (e.g. a logging listener and your command handler), the second gets an empty array. Pick one release point and enforce it. If you need to inspect without consuming, check whether the package exposes a non-draining accessor; otherwise add a peekEvents() helper rather than calling release twice.

Recording order is dispatch order. Events fire in the order the aggregate recorded them. Don't rely on Laravel's listener ordering to fix a domain-ordering problem — record them in the right sequence.

No auto-discovered service provider. With 0 dependents and 0 stars, expect to wire the EventBus binding yourself. If you publish the binding in a package of your own, use bind (not singleton) only if the bus is stateless — a Laravel dispatcher wrapper is stateless, so singleton is fine.

Serialization of domain events is your problem, not the package's. The contract guarantees a payload accessor, not a JSON-safe one. If you queue events or store them in an outbox, add __serialize/__unserialize or a dedicated serializer and test round-tripping explicitly.

Version pinning and source-reading. With a 0.x-ish, zero-star package, treat the public API as advisory. Pin an exact version in composer.json, and when you upgrade, diff src/ rather than trusting the changelog. The surface is small enough that a 10-minute read covers it.

Extension points worth knowing: implement DomainEvent directly for custom event bases; implement EventBus for adapters (Laravel dispatcher, Symfony messenger, a no-op for tests); subclass or wrap the event collection if you need filtering or deferred dispatch. Swapping the bus is the cheapest seam — build your test double there (RecordingEventBus that appends to an array) rather than mocking the interface with PHPUnit.

Don't let the trait leak into Eloquent models. If you use Eloquent, keep RecordsEvents on domain objects and map to/from persistence, or you'll fight the ORM's own model events (saved, created) which have nothing to do with domain events and will confuse the next developer.

Debugging tip: when listeners "don't fire," check in this order — was releaseEvents() actually called, was it called twice, did DB::afterCommit run (it won't inside a rolled-back or test-wrapped transaction), and is the EventBus binding the real one or a fake left over from a previous test.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
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