-
Notifications
You must be signed in to change notification settings - Fork 2
Stopping Propagation
The Events facade lets any listener veto the rest of
the chain by returning strict false. This page documents the exact
contract, the patterns it enables, and the (deliberate) lack of an
equivalent on EventEmitter::emit().
Events::trigger($name, ...$args) walks its listener list in
priority order (lower numeric priority first; FIFO within a
priority bucket — see Listeners & Priorities).
After each call it inspects the return value:
- If the listener returned
false(strict comparison,=== false), iteration stops immediately andtrigger()returnsfalse. - Otherwise the next listener runs.
- If all listeners ran without anyone returning
false,trigger()returnstrue. The sametrueis returned when no listeners are registered at all.
use InitPHP\Events\Events;
Events::on('publish', function (array $post) {
return $post['status'] === 'ready'; // false ⇒ veto, true ⇒ continue
});
Events::on('publish', function (array $post) {
/* indexer */
});
Events::on('publish', function (array $post) {
/* notifier */
});
if (Events::trigger('publish', $post) === false) {
http_response_code(409);
echo "Refused: post not ready.\n";
}The veto is the listener's say in whether the chain continues. There is no mechanism for the trigger site to override it.
Only strict false. Any other falsy value lets the chain continue:
| Returned value | Stops the chain? |
|---|---|
false |
✅ yes |
null |
❌ no |
'' (empty string) |
❌ no |
0 / 0.0
|
❌ no |
[] (empty array) |
❌ no |
| anything else | ❌ no |
Events::on('x', fn() => 0); // continues
Events::on('x', fn() => null); // continues
Events::on('x', fn() => false); // STOPSBe deliberate: if a listener forgets to return and the function falls
off the end (null), the chain keeps going. That is almost always what
you want — listeners should not accidentally veto.
EventEmitter::emit() returns void and ignores listener return values.
Every registered listener runs unconditionally, even if an earlier one
returned false:
use InitPHP\Events\EventEmitter;
$bus = new EventEmitter();
$bus->on('save', fn() => false);
$bus->on('save', fn() => print("still runs\n"));
$bus->emit('save'); // → still runsIf you need veto semantics with the low-level API, use a shared state object that listeners can mutate:
$ctx = (object) ['vetoed' => false];
$bus->on('save', function ($payload, $ctx) {
if (!$payload['ok']) {
$ctx->vetoed = true;
}
});
$bus->on('save', function ($payload, $ctx) {
if ($ctx->vetoed) {
return; // skip work
}
/* expensive write */
});
$bus->emit('save', [$payload, $ctx]);
if ($ctx->vetoed) {
/* react */
}Or just use the Events facade, which has the veto
contract built in.
A guard listener inspects the payload and short-circuits if it is invalid:
Events::on('user.delete', function (int $id) {
if (isProtected($id)) {
return false; // never delete protected users
}
});
Events::on('user.delete', $deleteFn);
Events::on('user.delete', $auditFn);
if (Events::trigger('user.delete', $id) === false) {
throw new RuntimeException('Deletion blocked.');
}The first listener tries the cache; subsequent listeners (DB read, deserialisation, …) only run on miss:
$result = null;
Events::on('lookup', function ($key) use (&$result) {
if ($hit = Cache::get($key)) {
$result = $hit;
return false; // skip DB
}
});
Events::on('lookup', function ($key) use (&$result) {
$result = Db::find($key);
});
Events::trigger('lookup', 'user:42');A pipeline of independent steps where any step may decide the request is already handled:
Events::on('webhook', fn ($req) => handleStripe($req) ?: null); // ?: null avoids accidental veto
Events::on('webhook', fn ($req) => handleGithub($req) ?: null);
Events::on('webhook', fn ($req) => handleDefault($req) ?: null);Each handler returns truthy/null in the normal case and explicit
false when it has consumed the request and wants the pipeline to stop.
-
Forgetting strict equality. The library uses
=== FALSE. If your listener returns0ornullit does not veto. This is by design. -
returnin a closure body. PHP closures without an explicitreturnproducenull— safe. But if you writefn() => doX(), the arrow returns whateverdoX()returned. IfdoX()can returnfalse, your closure can accidentally veto. -
No way to "resume". Once a listener returns
false, thattrigger()call is done. There is no exception to catch, no buffer to drain. If you need to know which listener vetoed, do it from inside the listener (log, set a flag, etc.) before returning. -
once()listeners are still cleaned up. If the chain halts at a listener earlier than a queuedonce()listener, the once-listener is still dropped after the trigger — the contract is "fire at most once", not "fire exactly once".Event::trigger()does this cleanup in atry/finallyblock, so it happens on exceptions too.
Eventsfacade- Listeners & Priorities
- Recipes — more end-to-end patterns.
initphp/events · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core APIs
Practical
Reference