A small, dependency-free event / hook library for PHP. Ships both a
high-level dispatcher with WordPress-style do_action-like semantics
(stop the chain when a listener returns false, simulate mode, debug
log) and a plain low-level EventEmitter you can instantiate and pass
around like any other object.
Heads-up — v2.0 fixes a long-standing priority-ordering bug. In 1.x, listeners ran in the order they were registered, not in the order their priorities asked for. v2.0 makes priority honour its name. If your 1.x code happened to register listeners in ascending priority order, the visible behaviour does not change; if it didn't, please re-read Priorities and ordering and the v2.0 changelog.
Events— a static facade backed by a lazily-built singleton. Convenient for application-wide hooks where threading a dispatcher through every layer is overkill.Event— the high-level dispatcher itself. Adds priority-ordered dispatch with a "returnfalsestops the chain" contract, plus optional simulate and debug modes. Use this directly when you want a dispatcher you own.EventEmitter— the low-level primitive. Plainon/once/emit/removeListener. No simulate, no debug, no short-circuit. Use this when you need a dispatcher with the smallest possible surface — or when you are migrating from the deprecatedinitphp/event-emitterpackage (the legacy\InitPHP\EventEmitter\*class names still work via a BC alias).
Everything is in a single PSR-4 namespace (InitPHP\Events\) and has
zero runtime dependencies.
| Requirement | Version |
|---|---|
| PHP | >= 5.6 (runtime); PHP >= 7.3 to run the test suite (PHPUnit 9.6) |
| Extensions | none |
| Composer dependencies | none |
composer require initphp/eventsIf you are coming from initphp/event-emitter, replace your dependency
— initphp/events declares a Composer replace for it and ships a
class alias so the old fully-qualified names still resolve. See
Migration for details.
require __DIR__ . '/vendor/autoload.php';
use InitPHP\Events\Events;
Events::on('helloTrigger', function (): void {
echo 'Hello World' . PHP_EOL;
}, 100);
Events::on('helloTrigger', function (): void {
echo 'Hi, World' . PHP_EOL;
}, 99);
Events::trigger('helloTrigger');Output:
Hi, World
Hello World
The listener registered with priority 99 runs before the one with
priority 100 — lower numeric value means runs first. The three
named constants Events::PRIORITY_HIGH (10), Events::PRIORITY_NORMAL
(100), and Events::PRIORITY_LOW (200) exist for readability.
Events::on('greet', function (string $name, string $me): void {
echo "Hi {$name}. I am {$me}." . PHP_EOL;
}, 99);
Events::trigger('greet', 'World', 'John');
// Hi World. I am John.A listener that returns boolean false halts the chain — subsequent
listeners are not invoked and trigger() itself returns false. This
is the same convention as WordPress's apply_filters short-circuit:
Events::on('save', function ($payload) {
if (!isValid($payload)) {
return false; // stops the chain
}
}, 10);
Events::on('save', function ($payload): void {
persist($payload); // never runs if validation returned false
}, 20);
if (!Events::trigger('save', $payload)) {
// at least one listener vetoed the operation
}If you prefer a dispatcher you instantiate and pass around (easier to
test, easier to scope, no global state), use Event:
use InitPHP\Events\Event;
$dispatcher = new Event();
$dispatcher
->on('user.registered', function ($user): void {
sendWelcomeEmail($user);
})
->on('user.registered', function ($user): void {
trackSignup($user);
}, Event::PRIORITY_LOW); // run last
$dispatcher->trigger('user.registered', $user);Event and Events expose the same surface — Events::on(...) simply
forwards to a shared Event instance.
For libraries that want the smallest possible surface, the underlying emitter is also public:
use InitPHP\Events\EventEmitter;
$emitter = new EventEmitter();
$emitter->on('hello', function (string $name): void {
echo "Hello {$name}!" . PHP_EOL;
}, 99);
$emitter->once('hello', function (string $name): void {
echo "Hi {$name}, this only fires once." . PHP_EOL;
}, 10);
$emitter->emit('hello', ['World']);
$emitter->emit('hello', ['World']);Differences from Event:
emit()returnsvoid(no short-circuit onfalse).- Listener arguments come in as a single array, not as varargs.
- No simulate or debug mode.
- No singleton.
EventEmitter is what Event is built on. The same instance is
reachable via $dispatcher->getEmitter() if you ever need both
high-level dispatch and low-level emit on the same listener registry.
PRIORITY_HIGH = 10 ← runs first
PRIORITY_NORMAL = 100
PRIORITY_LOW = 200 ← runs last
Rules:
- Lower numeric priority runs first. The names are about importance (high-priority work runs early); the numbers are about position.
- Within the same priority, listeners run in registration order (FIFO).
- Priority is global per-event — once-listeners and regular listeners are merged into a single priority-sorted queue when an event fires.
- Event names are matched case-insensitively:
on('User.Created')andemit('user.created')see the same listener.
See docs/04-priorities-and-ordering.md
for the full ordering contract, including how once() interacts with
the priority queue.
$dispatcher->once('boot', $listener); // fires at most once
$dispatcher->off('boot', $listener); // removes a specific listener
$dispatcher->removeAllListeners('boot'); // wipes one event
$dispatcher->removeAllListeners(); // wipes every eventThe once() contract is honoured even when the chain is stopped by a
false return — a one-shot listener that runs (or that would have
run, but for a short-circuit earlier in the queue) is dropped after
the trigger completes. See
docs/05-once-and-removal.md.
Event (and therefore Events) carry two opt-in instrumentation
modes:
$dispatcher = new InitPHP\Events\Event();
$dispatcher->setSimulate(true); // listeners are not invoked; trigger() still returns true
$dispatcher->setDebugMode(true); // each trigger() appends {start, end, event} to the log
$dispatcher->trigger('checkout.complete', $order);
$dispatcher->getDebug(); // [['start' => ..., 'end' => ..., 'event' => 'checkout.complete']]
$dispatcher->clearDebug(); // empty the logSee docs/06-debug-and-simulate.md.
| Class | Purpose |
|---|---|
InitPHP\Events\Events |
Static facade; thin shim over a shared Event instance. |
InitPHP\Events\Event |
High-level dispatcher (priority, short-circuit, simulate, debug). |
InitPHP\Events\EventEmitter |
Low-level primitive (on / once / emit / removeListener / clearOnceListeners). |
InitPHP\Events\EventEmitterInterface |
Contract that EventEmitter implements. |
| Method | Class | Purpose |
|---|---|---|
trigger(string $name, ...$arguments): bool |
Event · Events |
Dispatch with priority + short-circuit semantics. |
on(string, callable, int $priority = PRIORITY_NORMAL): self |
Event · Events · EventEmitter |
Register a listener. |
once(string, callable, int $priority = PRIORITY_NORMAL): self |
Event · Events · EventEmitter |
Register a one-shot listener. |
off(string, callable): self |
Event · Events |
Remove a specific listener (alias for removeListener). |
removeListener(string, callable): void |
EventEmitter |
Remove a specific listener. |
removeAllListeners(?string = null): self/void |
Event · Events · EventEmitter |
Wipe one event, or every event. |
emit(string, array $args = []): void |
EventEmitter |
Low-level dispatch (no short-circuit). |
clearOnceListeners(?string = null): void |
EventEmitter |
Drop one-shot listeners without invoking them. |
listeners(?string = null): array |
EventEmitter |
Inspect the current listener registry. |
setSimulate(bool): self / getSimulate(): bool |
Event · Events |
Toggle / inspect simulate mode. |
setDebugMode(bool): self / getDebugMode(): bool |
Event · Events |
Toggle / inspect debug mode. |
getDebug(): array / clearDebug(): self |
Event · Events |
Read / clear the debug log. |
getEmitter(): EventEmitter |
Event · Events |
Access the backing emitter. |
getInstance(): Event |
Events |
Return the shared dispatcher. |
setInstance(Event): void |
Events |
Inject a pre-configured dispatcher. |
reset(): void |
Events |
Drop the shared instance (tests, lifecycle resets). |
Full signatures and exception behaviour: docs/09-api-reference.md.
The standalone initphp/event-emitter package has been merged into
this one and is now deprecated. If your code currently uses
\InitPHP\EventEmitter\EventEmitter, no source changes are
required — this package ships a backwards-compatibility alias:
- "initphp/event-emitter": "^1.0",
+ "initphp/events": "^2.0"Composer will not install both packages side-by-side because
initphp/events:^2.0 declares a replace for initphp/event-emitter.
When you next touch the code, prefer the new canonical namespace:
// Before
use InitPHP\EventEmitter\EventEmitter;
// After
use InitPHP\Events\EventEmitter;The alias is intended as a transition aid and may be removed in a
future major release. One important behaviour change that comes
with v2.0: in 1.x, EventEmitter::emit() had a bug where the entire
listeners array was passed to call_user_func_array instead of each
individual listener, so emitted events silently fired no listeners at
all. That is fixed in 2.0 — if you had quietly broken emit() calls
in 1.x, they will now actually run their listeners.
See docs/07-migration-from-event-emitter.md for the full migration checklist.
The full guide lives under docs/:
- Getting started
- The
Eventsfacade - Using
EventEmitterdirectly - Priorities and ordering
- Once-listeners, removal, and cleanup
- Debug and simulate modes
- Migrating from
initphp/event-emitter - Recipes (plugin systems, request lifecycle, WordPress-style hooks)
- API reference
composer install
composer test # PHPUnitCI runs the test suite across PHP 7.3 – 8.4 and also lints the source
on PHP 5.6 / 7.0 / 7.1 / 7.2 so the php >= 5.6 library contract stays
honest.
Released under the MIT License.