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

Event Dispatcher Laravel Package

symfony/event-dispatcher

The EventDispatcher component lets application components communicate by dispatching events and listening to them. Register listeners on named events, control priority, and decouple your PHP code with a simple, standalone Symfony library.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pick a version

composer require symfony/event-dispatcher:^7.4   # LTS, PHP 8.1+
# or ^8.1 for PHP 8.4+ (strict return types enforced)

In a Laravel app this package is not a replacement for Illuminate\Events\Dispatcher — Laravel ships its own dispatcher and Event:: facade. You add Symfony's when you need a PSR-14 dispatcher (Psr\EventDispatcher\EventDispatcherInterface) to satisfy a third-party library, or you want a framework-agnostic domain event bus you can lift out of Laravel later. Keep the two worlds separate and bridge explicitly (see below).

Read these three files first (≈15 minutes total)

  • vendor/symfony/event-dispatcher/EventDispatcher.php — ~200 lines; the whole component.
  • vendor/symfony/event-dispatcher/EventSubscriberInterface.php
  • vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php — what you should type-hint against.

First use case: a domain event inside a service

// app/Domain/Order/OrderPlaced.php
final readonly class OrderPlaced
{
    public function __construct(
        public string $orderId,
        public int $totalCents,
    ) {}
}
// app/Providers/EventBusServiceProvider.php
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\EventDispatcher\EventDispatcherInterface;

public function register(): void
{
    $this->app->singleton(EventDispatcher::class, fn () => new EventDispatcher());

    $this->app->alias(EventDispatcher::class, EventDispatcherInterface::class);
}
// anywhere
public function store(EventDispatcherInterface $events)
{
    $order = Order::create(...);
    $events->dispatch(new OrderPlaced($order->id, $order->total_cents));
}

At this point nothing listens — that's correct. Add listeners only as the number of side effects grows (email, ledger entry, webhook). If you only ever have one side effect, use a plain service call and skip the bus.

Implementation Patterns

Closure/__invoke listeners registered at boot

$dispatcher->addListener(OrderPlaced::class, function (OrderPlaced $e) {
    SendOrderReceipt::dispatch($e->orderId);
}, priority: 0);

Register by FQCN, not by a string alias. Class names survive refactors and IDE renames; string aliases silently stop firing when you move a namespace.

Subscribers for grouping

final class OrderSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderPlaced::class  => [['onPlaced', 10], ['audit', -10]],
            OrderRefunded::class => 'onRefunded',
        ];
    }

    public function __construct(private Ledger $ledger, private Audit $audit) {}

    public function onPlaced(OrderPlaced $e): void { $this->ledger->post($e); }
    public function audit(OrderPlaced $e): void    { $this->audit->write('order.placed', $e); }
    public function onRefunded(OrderRefunded $e): void { /* ... */ }
}

Register the subscriber class as a Laravel singleton and wire it once in a service provider:

$this->app->singleton(OrderSubscriber::class);
$this->app->afterResolving(EventDispatcher::class, function (EventDispatcher $d) {
    $d->addSubscriber($this->app->make(OrderSubscriber::class));
});

Priorities are how you sequence cross-cutting concerns

Higher integer runs first; default is 0. Typical layout: 100 = validation/guard (may stop propagation), 50 = state mutation, 0 = notification, -100 = logging/metrics. Don't rely on registration order for same-priority listeners — service provider order is not something you want in your critical path.

Stoppable events for a real veto

final class CheckoutStarted implements StoppableEventInterface
{
    private bool $stopped = false;
    public function isPropagationStopped(): bool { return $this->stopped; }
    public function stop(): void { $this->stopped = true; }
}

A fraud-check listener at priority 100 calls stop(); the downstream "charge card" listener never runs. dispatch() still returns the event object, so the caller can inspect it.

Bridging to Laravel's dispatcher (the pattern you'll actually need)

Don't try to make one dispatcher impersonate the other. Instead, make one direction explicit:

// Symfony-side listener that hands off to Laravel's world
$dispatcher->addListener(OrderPlaced::class, function (OrderPlaced $e) {
    event(new \App\Events\OrderWasPlaced($e->orderId)); // Laravel events, queues, broadcast
}, -50);

This gives you PSR-14 for library-facing code and Laravel's ShouldQueue, broadcasting, and Event::fake() for everything else. Reverse the arrow (Laravel listener → Symfony dispatch) only if you're migrating.

Testing without booting Laravel

The dispatcher is a plain object, which makes domain tests fast:

$dispatcher = new EventDispatcher();
$seen = [];
$dispatcher->addListener(OrderPlaced::class, function ($e) use (&$seen) { $seen[] = $e->orderId; });

(new Checkout($dispatcher))->handle($cart);
$this->assertSame(['ord_1'], $seen);

For integration tests inside Laravel, Event::fake() will not intercept Symfony dispatches — assert against your own spy listener or the real downstream effect.

Optional attribute-based registration

#[AsEventListener] lives in FrameworkBundle, not in this package — you don't get it in Laravel. If you want that ergonomics, write ~20 lines in a service provider that reflects over app_path('Listeners') and calls addListener() from the attribute arguments. Worth it only once you have more than a handful of listeners.

Gotchas and Tips

  • Two dispatchers, zero crossover. Event::listen() does not see Symfony dispatches and vice versa. This is the #1 source of "my listener isn't firing." Grep for both dispatch( and event( before debugging anything else.
  • Signatures differ. Symfony: dispatch(object $event, ?string $eventName = null). Laravel: dispatch($event, $payload = [], $halt = false). Passing a string as the first argument worked pre-5.x and now throws TypeError. Always pass an object.
  • Event name defaults to the FQCN when omitted, but only if you also registered under the FQCN. Mixed string/FQCN registration is a silent failure mode.
  • Listener exceptions abort the chain. There is no error isolation. A failing SendOrderReceipt listener kills the remaining listeners and bubbles to the caller. For anything touching the network or a third party, make the listener a ShouldQueue Laravel job, or wrap in try/catch + report().
  • The dispatcher holds listeners forever. Closures capturing $this (a controller, a request) are a memory leak under Octane and long-running queue workers. Register singletons in a service provider, not per-request. The TraceableEventDispatcher had exactly this leak fixed in 6.4.36 / 7.4.8 / 8.0.8 — stay on a patched patch release.
  • TraceableEventDispatcher is dev-only. Wrap it with symfony/stopwatch, enable it behind app()->isLocal(), and call reset() between requests if you run Octane (also fixed for reset-during-dispatch in 6.4.37 / 7.4.9 — again, patch level matters).
  • GenericEvent is legacy. It's a stringly-typed bag of arguments with no IDE support. Define one small readonly class per event; it costs 8 lines and pays for itself the first time you rename a property.
  • Avoid removeListener() with closures. You need the identical callable instance; store the closure in a property if you must remove it, or use a subscriber object and removeSubscriber().
  • Bind as a singleton. Laravel's container will happily new a fresh EventDispatcher on every resolution because it has no constructor dependencies — meaning listeners registered on one instance are invisible to another. $this->app->singleton(...) is not optional.
  • Type-hint the contract, not the class. Psr\EventDispatcher\EventDispatcherInterface keeps your domain code swappable and lets you drop in a Laravel-backed adapter later without touching call sites.
  • Version choice matters more than usual. Symfony 8 forces return types on all components — any custom class extending EventDispatcher or implementing these interfaces breaks on upgrade. For a Laravel app, ^7.4 (LTS) is the low-risk pick.
  • Don't inject the dispatcher into Eloquent models. Dispatch from application services or model observers, so the model stays testable and the event's meaning isn't hidden in a booted() hook.
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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk