Events
Middleware covers the request pipeline. Events cover everything that isn’t pipeline-shaped — domain and framework moments you want to react to: the kernel booting, a route matching, an action about to run, a response about to be sent. Quiote ships a PSR-14 dispatcher and a set of lifecycle events, and it’s the primary extension point plugins hook into.
Like logging and telemetry, the event registry is a process-global, worker-lifetime facade — you register listeners once (at boot, usually from a plugin) and they persist across requests. With no listeners, the whole system is a single array lookup per event: it costs nothing when unused.
How events fit in a request
Section titled “How events fit in a request”The framework never scans for listeners; you register them explicitly against the Events registry — from a plugin’s register() (via $registrar->listen(...)) or in the front controller before Kernel::run(). Once registered, they stay for the worker’s life. The framework then fires its own lifecycle events at fixed points as a request flows through the pipeline:
Kernel boots (
KernelBootEvent) → request enters the pipeline →RoutingMiddlewarematches a route (RequestMatchedEvent) → the action runs (ActionBeforeEvent→ActionAfterEvent) →ContextRequestHandler::handle()returns (ResponseSendingEvent) → response emitted.
Each event is a plain readonly object carrying the relevant state; your listener runs synchronously at that point. See The request lifecycle for the surrounding flow.
Listening
Section titled “Listening”Register a listener through the Quiote\Event\Events facade, keyed by event class, with an optional priority (higher runs first):
use Quiote\Event\Events;use Quiote\Event\Lifecycle\RequestMatchedEvent;
Events::listen(RequestMatchedEvent::class, function (RequestMatchedEvent $e): void { // runs after routing resolves, before the action // $e->request, $e->module, $e->action, $e->routeName, $e->outputType}, priority: 0);A listener registered on a base class or interface also sees subclasses — so you can listen broadly (e.g. on the Event base) or narrowly (on one concrete event).
Register listeners where the registry is set up once per worker: in a plugin’s register() (via $registrar->listen(...)), or in your front controller before Kernel::run().
The lifecycle events
Section titled “The lifecycle events”All live in Quiote\Event\Lifecycle:
| Event | Fires | Payload (readonly) |
|---|---|---|
KernelBootEvent | end of Quiote::bootstrap() | environment, contexts |
RequestMatchedEvent | RoutingMiddleware, after a successful match | request, module, action, routeName, outputType |
ActionBeforeEvent | before ActionExecutor::execute() | descriptor — stoppable |
ActionAfterEvent | after the action runs | descriptor, execution result |
ResponseSendingEvent | ContextRequestHandler::handle(), just before returning | request, response |
use Quiote\Event\Events;use Quiote\Event\Lifecycle\KernelBootEvent;
Events::listen(KernelBootEvent::class, function (KernelBootEvent $e): void { // one-time per-worker setup — $e->environment, $e->contexts});Dispatching your own events
Section titled “Dispatching your own events”Define an event (a plain object) and dispatch it. The dispatcher returns the same event, so listeners can enrich it:
namespace App\Event;
use Quiote\Event\Event;
final class OrderPlaced extends Event{ public function __construct(public readonly int $orderId) {}}use Quiote\Event\Events;use App\Event\OrderPlaced;
Events::dispatch(new OrderPlaced($order->id));Stoppable events
Section titled “Stoppable events”Extend Quiote\Event\StoppableEvent to let a listener halt propagation (subsequent listeners are skipped). ActionBeforeEvent is stoppable, for example. Dispatch honours isPropagationStopped() per PSR-14.
Deferring event construction until something listens
Section titled “Deferring event construction until something listens”emit() still allocates the event object before it can check whether anything listens — fine for a cheap event, wasteful for one that carries an expensive-to-build payload. Events::emitLazy() checks hasListeners() first and only invokes your factory closure (and therefore only constructs the event) when a listener actually exists:
use Quiote\Event\Events;use App\Event\OrderPlaced;
Events::emitLazy(OrderPlaced::class, fn() => new OrderPlaced($order->id, $order->computeExpensiveSummary()));Note that the first argument is the event class name, not an instance — that’s what lets the listener check happen before the factory runs. It returns the dispatched event, or null if nothing listened (in which case the factory never ran). Listener exceptions are handled the same way emit() handles them — logged, not propagated. This is what the framework’s own lifecycle emit sites use internally (route matched, action before/after, response sending, worker request completed).
dispatch() vs emit() — who absorbs a bad listener
Section titled “dispatch() vs emit() — who absorbs a bad listener”The facade has two dispatch methods, and the distinction matters:
-
Events::dispatch($event)— PSR-14, fail-loud: a listener that throws propagates to the caller. Use this for your own events where you want to know a listener broke. -
Events::emit($event)— dispatches only if a listener exists, and swallows listener exceptions (logging them instead). The framework usesemit()at its lifecycle sites so a buggy listener can never take down a request or the boot — the same “never crash the request” posture as telemetry. -
Events::emitLazy($class, $factory)— asemit(), but constructs the event only if a listener exists. See Deferring event construction above.
Events::hasListeners($class) lets you check whether anything is listening without dispatching; prefer Events::emitLazy() when the point of checking is to skip constructing an expensive event. Events::dispatcher() returns the underlying EventDispatcher (a PSR-14 EventDispatcherInterface); Events::reset() clears the registry (for tests).
What events are not
Section titled “What events are not”Events are for reacting, not for the request pipeline itself. If you need to intercept, short-circuit, or reorder request handling — auth, CSRF, a health check — that’s middleware, not a listener. Events run at fixed lifecycle points and (via emit()) are deliberately unable to break the request.