thesis/dispatcher

Thesis Dispatcher

Maintainers

Package info

github.com/thesis-php/dispatcher

pkg:composer/thesis/dispatcher

Fund package maintenance!

www.tinkoff.ru/cf/5MqZQas2dk7

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.2.0 2026-05-07 19:35 UTC

This package is auto-updated.

Last update: 2026-05-07 19:37:30 UTC


README

A lightweight hook dispatcher for PHP. Handlers subscribe to hook classes and are called when those hooks are dispatched.

Installation

composer require thesis/dispatcher

Usage

Subscribing and dispatching

use Thesis\Dispatcher;

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

$dispatcher = new Dispatcher();

$dispatcher->subscribe(UserRegistered::class, function (UserRegistered $hook): void {
    echo "Welcome, {$hook->email}!\n";
});

$dispatcher->dispatch(new UserRegistered('user@example.com'));
// Welcome, user@example.com!

Unsubscribing

subscribe() returns an unsubscribe closure:

$unsubscribe = $dispatcher->subscribe(UserRegistered::class, function (): void {
    // ...
});

$unsubscribe(); // removed, will not be called on next dispatch

Unsubscribing from within a handler

The handler receives an unsubscribe closure as its second argument — useful for one-shot handlers:

$dispatcher->subscribe(UserRegistered::class, function (UserRegistered $hook, \Closure $unsubscribe): void {
    sendWelcomeEmail($hook->email);
    $unsubscribe(); // run once, then remove self
});

Multiple handlers

All handlers subscribed to the same hook are called in subscription order:

$dispatcher->subscribe(UserRegistered::class, $sendWelcomeEmail);
$dispatcher->subscribe(UserRegistered::class, $createDefaultSettings);
$dispatcher->subscribe(UserRegistered::class, $notifyAdmins);

$dispatcher->dispatch(new UserRegistered('user@example.com'));
// all three handlers are called in order

Subscribing and unsubscribing during dispatch

It is safe to subscribe or unsubscribe handlers while a hook is being dispatched. Handlers added during dispatch are called in the same pass; unsubscribed ones are skipped:

$dispatcher->subscribe(UserRegistered::class, function (UserRegistered $hook, \Closure $unsubscribe) use ($dispatcher): void {
    echo "first\n";
    $unsubscribe(); // removes itself — skipped on future dispatches, does not affect others

    $dispatcher->subscribe(UserRegistered::class, function (): void {
        echo "third\n"; // added mid-dispatch — still called in this same pass
    });
});

$dispatcher->subscribe(UserRegistered::class, function (): void {
    echo "second\n";
});

$dispatcher->dispatch(new UserRegistered('user@example.com'));
// first
// second
// third

$dispatcher->dispatch(new UserRegistered('user@example.com'));
// second
// third

Hook classes must be final

All hook classes must be declared final (enums are also accepted, as they are implicitly final).

The dispatcher matches hooks by exact class name. If inheritance were allowed, a handler subscribed to a parent class would silently not fire for subclass instances — which is confusing. Requiring final makes this contract explicit: one class, one set of handlers, no surprises.

It also nudges toward treating hooks as simple sealed value objects, which is the right model for them.

PSR-14

This library deliberately does not implement psr/event-dispatcher. A few reasons:

  • The PSR calls them events; we prefer hooks — a subtly different mental model that better reflects push-based notifications rather than something that "happened" in the domain.
  • StoppableEventInterface conflates stopping propagation with the event itself, which we consider a design smell. Stopping propagation is a dispatcher concern, not a data concern.
  • PSR-14 adoption in the ecosystem is limited, so the interoperability argument is weak in practice.

If you do need PSR-14 compatibility, writing a thin adapter is straightforward.

License

MIT