gpslab/domain-event-bundle
Symfony bundle that integrates gpslab/domain-event with the framework, making it easy to publish and handle domain events through Symfony services and configuration. Lightweight setup for decoupled domain events in Symfony applications.
gpslab/domain-event-bundle is a Symfony bundle, not a Laravel package — there is no composer.json extra.laravel block, no auto-discovery, no publishable config. composer require gpslab/domain-event-bundle will install it, but nothing will happen until you wire the services yourself. Given 14 stars, zero dependents and a last release of May 2021, treat it as a reference implementation you can vendor or reimplement rather than a dependency you inherit.
Read these three files first; they define the entire contract:
GpsLab\Component\DomainEvent\EventBus\EventBusInterface — the whole API is basically publish(DomainEvent $event): void.GpsLab\Component\DomainEvent\Event\SubscriberInterface — getSubscribedEvents(): array mapping event FQCN → method name(s), mirroring Symfony's EventSubscriberInterface.GpsLab\Component\DomainEvent\Aggregate\PullDomainEventsTrait — raise() / releaseEvents() for aggregate roots.The first use case is the canonical one: an aggregate records events internally, and the application layer publishes them after persisting.
final class Order
{
use PullDomainEventsTrait;
public function pay(): void
{
// ... invariants, state change ...
$this->raise(new OrderPaid($this->id));
}
}
// in the use case / command handler
foreach ($order->releaseEvents() as $event) {
$this->bus->publish($event);
}
That's the mental model. Everything else — the deferred bus, the container listener locator, the compiler pass — is plumbing to make that loop lazy, transactional, or framework-native.
Subscriber vs. listener. Two registration styles ship with the package: ListenerInterface::handleEvent(DomainEvent $event) for single-event classes, and SubscriberInterface::getSubscribedEvents() for classes that fan out to several events. Prefer subscribers — one class per bounded-context concern (e.g. OrderNotifier handling OrderPaid, OrderRefunded) beats one class per event.
Two bus implementations. EventBus is a plain in-memory implementation given a ListenerLocatorInterface; SymfonyEventBus adapts to Symfony's EventDispatcher, registering the domain event class name as the dispatcher event name. In Laravel you will almost certainly skip SymfonyEventBus (Laravel's dispatcher has a different signature and its own subscriber contract) and use EventBus directly, or write a five-line adapter around Laravel's Event::dispatch.
Deferred publishing for transactions. DeferredEventBus wraps the real bus and queues events until you call release() (with clear() to drop them). This is the pattern you want around any state-changing use case:
DB::transaction(function () use ($order, $bus) {
$orders->save($order);
foreach ($order->releaseEvents() as $event) {
$bus->publish($event);
}
});
$bus->release(); // outside the transaction
If you'd rather not manage release ordering, Laravel's DB::afterCommit() gives you the same semantics with less rope.
Laravel service provider. Keep it small — bind the interface, provide the locator:
$this->app->singleton(ListenerLocatorInterface::class, function ($app) {
return new ContainerListenerLocator($app, [
OrderPaid::class => [OrderNotifier::class],
]);
});
$this->app->singleton(EventBusInterface::class, fn ($app) =>
new EventBus($app->make(ListenerLocatorInterface::class))
);
Bridging to Laravel's event system. If you want queued listeners and Event::fake() in tests, have a single EventBus listener that forwards into Event::dispatch($event) and let Laravel do the routing. You lose getSubscribedEvents() and gain jobs, ShouldQueue, and the testing helpers — usually the right trade.
Reimplementing. The component is maybe 150 lines total. A DomainEventBus with a [$eventClass => [$listener, ...]] map plus a deferred variant is an afternoon's work and removes the Symfony DI coupling entirely. With this package's adoption numbers, that is a legitimate answer.
getSubscribedEvents container wiring. The bundle's compiler pass (RegisterListenersCompilerPass / subscriber tagging) is Symfony DI-specific and does nothing in Laravel. You must build the event-class→listener map yourself — via config, attributes, or a directory scan.release() and nothing throws; events simply never dispatch. Wrap it in a finally or in DB::afterCommit() so it can't be dropped.EventBus::publish() invokes listeners inline. A listener that publishes another event that reaches the same listener is an infinite loop. Watch for it especially with deferred release, where a large queued batch cascades.ContainerListenerLocator wants a PSR-11 container. Laravel's Application implements it, so app() works — but resolve lazily, never eagerly at boot, or you'll instantiate every listener on every request.EventDispatcherInterface::dispatch(object, string) ordering will bite if you use SymfonyEventBus. Test before assuming compatibility.ShouldQueue, dispatch()->afterCommit()), not a package feature.ListenerLocator with spy listeners gives you a clean unit test of an aggregate without booting the framework. Assert on releaseEvents() output from the aggregate and on listener invocations separately — don't conflate them.ListenerLocatorInterface for a custom locator (attribute-driven, PSR-4 directory scan) and swap EventBusInterface for a decorated version (logging, transactional, queueing). Both are one-method interfaces, which is the package's genuine virtue — the interfaces are more useful than the implementations.How can I help you explore Laravel packages today?