The reference event dispatcher for the Milpa PHP framework, built on
milpa/core. String-named events with dot-segment wildcard subscriptions (user.*), priority ordering, per-listener error isolation, and a pluggable async (queue) seam — the concrete implementation of theMilpaEventDispatcherInterfacecontractmilpa/coredefines.
milpa/events is where milpa/core's event-dispatch seam becomes a working engine.
Milpa\Interfaces\Event\MilpaEventDispatcherInterface is a contract defined in core; this
package is the concrete EventDispatcher — plugins publish and subscribe to string-named
events ('user.created', 'order.shipped') without depending on each other directly.
No Doctrine, no HTTP kernel, no concrete queue — the async seam is a plain callable you
wire in; the queue itself lives in your host application.
composer require milpa/eventsSubscribe by exact name or by a dot-segment wildcard, dispatch, and let priority decide the call order — higher priority runs first:
use Milpa\Eventing\EventDispatcher;
use Psr\Log\NullLogger;
$dispatcher = new EventDispatcher(new NullLogger());
$dispatcher->subscribe('user.*', function (string $event, array $payload): void {
// catches every one-segment event under `user.` — user.created, user.updated, ...
});
$dispatcher->subscribe('user.created', function (string $event, array $payload): void {
// runs before the wildcard handler above: higher priority
}, priority: 10);
$dispatcher->dispatch('user.created', ['id' => 42]);
$dispatcher->hasSubscribers('user.created'); // true
$dispatcher->hasSubscribers('order.created'); // false — no exact or wildcard matchA handler that throws is logged and does not stop the remaining handlers — one bad listener never aborts a dispatch:
$dispatcher->subscribe('order.placed', fn () => throw new \RuntimeException('boom'));
$dispatcher->subscribe('order.placed', fn () => /* still runs */ null);
$dispatcher->dispatch('order.placed'); // both handlers ran; the exception was logged, not thrownEvent names are dot-separated segments; * matches exactly one segment — it never spans
a .. Matching is case-sensitive and anchored (the whole name must match):
| Pattern | Matches | Does not match |
|---|---|---|
user.* |
user.created, user.deleted |
user.profile.updated (two segments after user.) |
*.created |
user.created, order.created |
user.createdX (anchored, not a substring match) |
* |
boot, ready (single-segment names) |
user.created (dotted) |
dispatch($event, $payload, async: true) requests deferred execution. This package ships
no queue — you wire one in with setAsyncDispatcher():
$dispatcher->setAsyncDispatcher(function (string $event, array $payload): void {
// hand off to your queue (Symfony Messenger, a Doctrine-backed job table, ...)
});
$dispatcher->dispatch('order.placed', ['id' => 1], async: true); // -> queue, not inlineWithout a dispatcher wired, async: true degrades to synchronous dispatch (subscribers
run inline, in the same call) rather than silently dropping the event — a conformant
fallback per the interface's documented $async contract, not a bug. Both branches are
covered in tests/EventDispatcherTest.php.
milpa/core already owns Milpa\Events\ for its event objects
(VerificationRequestedEvent and friends, under src/Events/). A package literally named
milpa/events colliding with that namespace would either clash or force an awkward split.
Milpa\EventDispatcher\EventDispatcher was the other candidate and stutters. Milpa\Eventing
avoids both: no collision, no stutter, and it names the mechanism (dispatch, subscribe,
wildcards, priority) distinctly from Milpa\Events, which names the event objects — the
two packages' responsibilities stay visibly distinct. This follows the family-wide rule:
namespace semantically correct beats cosmetic symmetry with the package name.
| Layer | Package | Owns |
|---|---|---|
| Contracts | milpa/core |
MilpaEventDispatcherInterface, EventSubscriberInterface — the seam, not the engine. |
| Dispatcher | milpa/events (this package) |
The concrete EventDispatcher: exact + wildcard subscriber matching, priority ordering, listener error isolation, and the async seam. |
| Your app | your host / plugins | The queue setAsyncDispatcher() hands events to, and any PSR-3 logger you pass in. |
- PHP ≥ 8.3
milpa/core^0.6psr/log^3
Full API reference: getmilpa.github.io/events — generated straight from the source DocBlocks and dressed with the Milpa design system.
Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.
Apache-2.0 © Rodrigo Vicente - TeamX Agency.
Milpa is designed, built, and maintained by Rodrigo Vicente - TeamX Agency.