innmind/event-bus
A lightweight event bus for PHP from the Innmind ecosystem. Publish domain events and register listeners or handlers to react to them, keeping producers and consumers decoupled behind a small, predictable API that fits neatly into larger Innmind applications.
The package is small and archived, so the fastest path is to read the source rather than the README. Start with composer.json (PHP constraint and the innmind/* dependency range), then the interface files in src/ — there are usually only two that matter:
EventBus — the dispatcher, effectively dispatch(object $event): voidListener — a callable object, effectively __invoke(object $event): voidEverything else is a concrete implementation of the bus plus the class-name matching strategy. Confirm the exact constructor names against src/ before writing code; the shape below follows the package's conventions.
composer require innmind/event-bus
use Innmind\EventBus\EventBus;
use Innmind\EventBus\Listener;
final class SendWelcomeEmail implements Listener
{
public function __construct(private Mailer $mailer) {}
public function __invoke(object $event): void
{
// guard, because the parameter is typed `object`
if (!$event instanceof UserRegistered) {
return;
}
$this->mailer->send(new WelcomeEmail($event->userId));
}
}
Wire the bus once (a container singleton or a small factory), register the listener, and dispatch:
$bus->dispatch(new UserRegistered($userId));
First use case: decouple side effects from a use case. In your RegisterUser handler, persist the user, then dispatch UserRegistered; let listeners own the email, the audit log entry, and the analytics call. That is the whole value proposition — the handler stops growing every time a new side effect appears.
Decorate the interface, don't fork the implementation. Because EventBus is an interface, the idiomatic extension is a wrapper:
final class LoggingBus implements EventBus
{
public function __construct(
private EventBus $inner,
private LoggerInterface $logger,
) {}
public function dispatch(object $event): void
{
$this->logger->info('event.dispatched', ['event' => $event::class]);
$this->inner->dispatch($event);
}
}
The same wrapper trick gives you a TransactionalBus (buffer events, flush after commit), a RetryingBus, or a SpyBus for tests.
Keep listeners as thin adapters. A listener should translate one event into one call on a domain service. Business logic inside a listener is invisible to the code that dispatched the event and is the most common way this pattern rots.
Events are immutable value objects. Past-tense names (UserRegistered, not RegisterUser), readonly/final classes, and payloads made of scalars, IDs, and value objects — not entities. If you ever move a listener behind a queue, the event has to survive serialization, and entity graphs never do.
Register listeners in one place. A single factory or container binding that builds the bus with its listener collection keeps the wiring discoverable. Registering at the call site ($bus->register(...) inside a controller) makes the dispatch graph untraceable.
Dispatch at the transaction boundary. Persist first, dispatch after the write succeeds. If listeners touch the same database and one throws, you now need to decide whether the outer transaction rolls back — usually you don't want that, which is exactly what the TransactionalBus decorator above solves.
Testing. For handler tests, inject the EventBus interface and swap in a spy that records dispatched events; assert on the recorded events rather than on listener side effects. For the bus itself, innmind/black-box (the author's property-testing library) is the expected harness if you want to write tests in the same style.
Under Laravel/framework integration, bind EventBus in a service provider as a singleton and resolve listeners from the container. Be deliberate about the boundary with Laravel's own Event facade — running both dispatchers over the same domain events doubles your side effects.
composer.json and be prepared to vendor the source. The upside: the surface is tiny, so forking or inlining it into your app is a realistic afternoon's work if you need to move off it.innmind/immutable or reflection constraint can block an upgrade elsewhere in the dependency graph. Check composer why-not before committing.SwallowErrors decorator so a flaky mailer cannot fail a successful registration.object, so listeners must guard with instanceof. If the package offers a ClassName namespace, that is the matching strategy used to resolve a listener from the event's class — check whether it matches superclasses/interfaces, and write your listener guards to agree with it.How can I help you explore Laravel packages today?