Skip to content

v0.2.6 — Events that are objects, and listeners sorted once

Choose a tag to compare

@FloKnapp FloKnapp released this 21 Sep 19:33
· 19 commits to main since this release
d0326f6

Two changes to EventManager, both about events being an extension mechanism rather than a convenience. Nothing that exists has to change.

An event can now be an object

event()->dispatch(new OrderShipped($order));
event()->listen(OrderShipped::class, fn(OrderShipped $e) => …);

The class is the name and the object is the payload. dispatch() accepts string|object; a string is still a name, so an application converts one event at a time or none.

listen() needed nothing at all. ::class is a string like any other name, so listen(OrderShipped::class, …) already worked before this release — there was simply nothing that would dispatch under that name. The whole change is four lines at the top of dispatch().

What it buys is what a string cannot. A misspelled class is an error where it is written, while a misspelled event name is a listener that never runs and never says so. An IDE can find every listener of an event and rename one as a refactoring. And the payload has a declared shape instead of a docblock describing variadic arguments — which matters most for the events a listener is meant to change something in, where the shape of what may be edited is the contract.

Nothing is required of an event class: no interface, no base class, no marker. Requiring one would buy nothing and would make moving an existing event over more than a one-line change.

One thing worth knowing before converting an existing event: string names and class names share one key space. That is why listen(X::class) already worked, and it means converting is a rename — a listener registered on the old string stops hearing it, silently. Convert the listeners in the same change.

Listeners are sorted when they are dispatched, not while they are registered

listen() sorted the whole list on every call, so registering n listeners for one event sorted a growing list n times — O(n² log n) to build something that is read once.

Listeners on one event before after (register + first dispatch)
25 0.027 ms 0.010 ms
100 0.531 ms 0.036 ms
500 19.15 ms 0.207 ms
1000 94.90 ms 0.465 ms

An application with a handful of listeners per event does not notice either way; at ten the difference is twenty microseconds. This is not an optimisation. Events are how this framework is extended, so how many listeners an event carries is somebody else's decision, and a quadratic ceiling nobody chose is the kind that gets found late and far from its cause.

A dirty flag per event rather than one sort, because a listener registered after an event has already been dispatched still has to take its place in the order rather than the end of the queue. That has a test of its own, as does the thing the sort has always quietly promised: listeners sharing a priority run in the order they were registered.

Upgrading

Nothing to do. Both changes are additive and the existing behaviour is unchanged — dispatch order, priorities, class listeners, return values and dispatchForResponse() all behave exactly as in v0.2.5.

143 tests, 291 assertions.