Typed events

Events are typed PHP objects passed to one or more listeners.

namespace App\Events;

final readonly class UserRegistered
{
    public function __construct(
        public int $userId,
        public string $email,
    ) {
    }
}

Readonly event objects are the preferred style when listeners should only observe what happened. An event may be mutable when its contract deliberately allows listeners to record a result or stop further processing.

Define a listener

Add #[Listener] to an invokable class. Gustav infers the event type from the single __invoke() parameter and injects constructor dependencies normally.

namespace App\Events;

use App\Services\WelcomeMailer;
use GustavPHP\Gustav\Attribute\Listener;

#[Listener]
final readonly class SendWelcomeEmail
{
    public function __construct(private WelcomeMailer $mailer)
    {
    }

    public function __invoke(UserRegistered $event): void
    {
        $this->mailer->send($event->userId, $event->email);
    }
}

A listener must be instantiable and declare one public, non-static __invoke() method with:

  • exactly one non-null event class or interface parameter
  • no scalar, union, or intersection event type
  • no variadic or by-reference event parameter
  • an explicit void return type

Invalid listener signatures fail application startup.

Dispatch an event

Inject the standard PSR-14 EventDispatcherInterface into a controller, service, middleware, command, or another listener:

use Psr\EventDispatcher\EventDispatcherInterface;

final readonly class RegisterUser
{
    public function __construct(
        private UserRepository $users,
        private EventDispatcherInterface $events,
    ) {
    }

    public function register(string $email): User
    {
        $user = $this->users->create($email);
        $this->events->dispatch(new UserRegistered($user->id, $user->email));

        return $user;
    }
}

Dispatch returns the same event object. Dispatching an event with no matching listeners is a no-op and does not throw.

Custom namespaces

Place #[Listener] classes under the application's Events namespace. Add listener namespaces from shared packages or modules to the project configuration:

Configuration::forProject(
    namespace: 'App',
    root: dirname(__DIR__),
    eventNamespaces: ['Module\Billing\Events'],
);

Listeners may type their parameter as a concrete event, parent class, or interface. Every compatible listener receives the event.

Priority and stopping propagation

Listeners with a higher priority run first. For equal priorities, listener class names determine the order. Prefer independent listeners and use priority only when ordering is part of the event contract.

#[Listener(priority: 100)]
final readonly class CheckImportPermission
{
    public function __invoke(BeforeUserImport $event): void
    {
        if (!$event->allowed) {
            $event->stop();
        }
    }
}

Implement PSR-14's StoppableEventInterface when an event supports stopping later listeners:

use Psr\EventDispatcher\StoppableEventInterface;

final class BeforeUserImport implements StoppableEventInterface
{
    private bool $stopped = false;

    public function __construct(public readonly bool $allowed)
    {
    }

    public function stop(): void
    {
        $this->stopped = true;
    }

    public function isPropagationStopped(): bool
    {
        return $this->stopped;
    }
}

An event already marked as stopped invokes no listeners.

Scope and failures

A listener and its scoped dependencies are reused during one HTTP request or application command, even when several matching events are dispatched. They are discarded when that request or command finishes.

Do not inject the scoped dispatcher into a singleton service. Dispatch from a scoped service, controller, middleware, command, or listener instead.

Dispatch is synchronous. A listener exception stops dispatch and propagates to the surrounding request or command boundary. HTTP requests use Gustav's normal safe error response and logging pipeline; commands use the normal safe failure message and exit code. Events do not provide queues, retries, or background execution.