An event dispatcher based on PSR-14: Event Dispatcher.
PSR-14 splits dispatching in two: something that decides which listeners an event has, and something that calls them. Most implementations hand you one object doing both, which is convenient until you want to know why a listener did or did not run.
Here they are the two classes the standard describes — a provider you register with, a dispatcher that calls what it is given — and a listener is any callable. There is no subscriber interface to implement, no compiler pass, and no event name to keep in step with a class name: an event is its class, and listening for a parent class or an interface catches everything that is one.
- PHP 8.1 or newer
composer require quillstack/eventsAn event is any object. A listener is anything callable which takes it:
use Quillstack\Events\EventDispatcher;
use Quillstack\Events\ListenerProvider;
$listeners = new ListenerProvider();
$listeners->listen(UserRegistered::class, function (UserRegistered $event) {
$this->mailer->welcome($event->email);
});
$dispatcher = new EventDispatcher($listeners);
$dispatcher->dispatch(new UserRegistered('radek@quillstack.com'));dispatch() gives the event back, so a listener can answer through it.
Listeners with a higher priority are called first, and two of the same priority are called in the order they were registered:
$listeners->listen(UserRegistered::class, $writeToTheLog, 10);
$listeners->listen(UserRegistered::class, $sendTheEmail);
$listeners->listen(UserRegistered::class, $cleanUpAfterwards, -10);A listener registered for a class is called for that class and for anything extending or implementing it, so listening for an interface catches every event carrying it:
$listeners->listen(HasUser::class, $recordWhoDidIt);An event extending StoppableEvent can be stopped by a listener, and the ones after it are
not called:
final class OrderPlaced extends StoppableEvent
{
}
$listeners->listen(OrderPlaced::class, function (OrderPlaced $event) {
if ($this->alreadyHandled($event)) {
$event->stopPropagation();
}
});Measured with quillstack/benchmark on a thousand events, each passing through five listeners. All three call the same five in the same order. Runs are interleaved and unconcurrent, each figure is the median of five, and PHP is 8.5.7.
| Version | |
|---|---|
| quillstack/events | 0.6.0 |
| symfony/event-dispatcher | v7.4.17 |
| league/event | 3.0.3 |
| Per event | Relative | |
|---|---|---|
| symfony/event-dispatcher | 0.34 µs | 0.39× |
| league/event | 0.47 µs | 0.53× |
| quillstack/events | 0.88 µs | — |
This one is the slowest of the three, and the reason is a decision rather than an oversight: the provider works out which listeners match an event every time it is asked, by testing the event against each registered class. That is what makes listening for a parent class or an interface work without anybody registering the subclass. Symfony looks the event name up in an array and sorts once.
Half a microsecond an event is not a thing to choose on. A request dispatching a hundred events spends fifty microseconds more here than on Symfony's, and if that is the shape of your application, Symfony's dispatcher is excellent and this is not trying to replace it.
composer testCoverage needs phpdbg:
composer test:coverageThis is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.
- quillstack/framework — where listeners are registered
- quillstack/queue — for work that should not happen now
- quillstack/di — what builds a listener that is a class
MIT. See LICENSE.