-
Notifications
You must be signed in to change notification settings - Fork 2
Troubleshooting
Most issues reduce to one of a few recurring shapes. Match your symptom to the section below.
The Events facade and new EventEmitter() are
separate registries. A listener added with Events::on() will not
fire when you call (new EventEmitter())->emit(), and vice versa.
// ❌ different registries
Events::on('e', $listener);
(new EventEmitter())->emit('e'); // listener does not run
// ✅ same registry
$bus = new EventEmitter();
$bus->on('e', $listener);
$bus->emit('e');Names are case-insensitive but whitespace-sensitive. The following pairs all match:
Events::on('User.Registered', …);
Events::trigger('user.registered'); // ✅ matches
Events::trigger('USER.REGISTERED'); // ✅ matchesBut these do not:
Events::on('user.registered', …);
Events::trigger('user.registered '); // ❌ trailing space
Events::trigger('user_registered'); // ❌ different separatorEvents::trigger('boot'); // fires zero listeners
Events::on('boot', $bootHandler); // too lateIn long-lived processes (workers, daemons) this is obvious, but it trips up scripts that build the listener list lazily. Register first, trigger second.
Events::setSimulate(true) makes trigger() walk the list without
calling any listener. If a previous test or CLI flag left it on,
nothing visible happens.
var_dump(Events::getSimulate()); // expect falseEvents::trigger() stops the moment any listener returns strict
false. Check the return value:
$ok = Events::trigger('save');
if ($ok === false) {
echo "A listener vetoed the chain.\n";
}Toggle debug mode briefly to inspect which listeners actually executed:
Events::setDebugMode(true);
Events::trigger('save');
print_r(Events::getDebug());See Stopping Propagation.
The package throws this whenever an argument fails a basic type or shape check:
| Message contains | Cause |
|---|---|
$event must be a string |
You passed null, an int, or an object to on()/once()/emit()/trigger(). |
$listener must be a callable |
The second argument to on()/once()/removeListener() was not a callable. Most often a typo in a function name or an [$obj, 'method'] pair where the method does not exist. |
$priority must be an integer |
The third argument was a string like '100' or a float. |
$arguments must be an array |
The second argument to EventEmitter::emit() was not an array. (Events::trigger uses varargs, so this only affects the low-level API.) |
$simulate must be a boolean |
Passed a non-bool to Events::setSimulate(). |
$debugMode must be a boolean |
Passed a non-bool to Events::setDebugMode(). |
Fix the call site; there is no global toggle to weaken these checks.
If you upgraded from initphp/event-emitter:^1.0 and now your
emit() calls fire listeners that previously seemed silent, that is
expected: the 1.x line of the standalone package shipped with a bug
where the entire listeners array (rather than each listener) was passed
to call_user_func_array, so listeners never ran. The bug is fixed in
initphp/events:^2.0.
If your old code relied on listeners not firing, audit those code paths before deploying. See Migration Guide.
If you are on initphp/events:^2.0, this should not happen — the
dispatcher honours priority order (lower numeric value runs first;
within a priority bucket, registration order). Check:
-
Are you actually on 2.0?
composer show initphp/events. The priority bug was in 1.x. -
Are your priorities really different? Listeners that share a
priority run in registration order; if both are
PRIORITY_NORMAL, the one registered first wins. - Is the listener you're staring at attached to a different event name (case-folded differently)? See the "event names matched" section above.
If you are still on 1.x and seeing registration-order behaviour, that was the 1.x bug. Upgrade to 2.0; see Migration Guide.
Full ordering contract: Listeners & Priorities.
EventEmitter::removeListener() uses === to identify the listener
you registered. Two separate closures, even with identical bodies, are
not equal:
$bus->on('e', function () { /* … */ });
// later
$bus->removeListener('e', function () { /* … */ }); // ❌ no-opKeep a reference:
$listener = function () { /* … */ };
$bus->on('e', $listener);
// later
$bus->removeListener('e', $listener); // ✅ worksOr use a named callable form (string function name, [$obj, 'method'],
or an invokable object) — those are easier to pass back later.
Debug mode accumulates one row per listener invocation. Either:
- Call
Events::clearDebug()at the boundary of each job/request to empty the log without disabling debug mode. - Call
Events::reset()at the same boundary to drop the singleton entirely (clears the debug log, the simulate / debug flags, and every registered listener — re-register on the next iteration). - Disable debug mode (
Events::setDebugMode(false)) when you no longer need it. Subsequent triggers stop adding rows; existing rows stay until youclearDebug()/reset()or the process exits. - Use the
EventEmitterand roll a tiny profiler of your own —microtime(true)plus an array — with whatever retention policy you need.
See Simulate & Debug Mode › Debug mode.
Make sure you ran composer dump-autoload after installing, and that
your script is loading vendor/autoload.php. The package exports two
roots:
-
InitPHP\Events\*(canonical PSR-4 →src/) -
InitPHP\EventEmitter\*(alias, loaded viasrc/aliases.php)
The alias is wired up by Composer's files autoload, so it is
available the instant vendor/autoload.php is included. If you are
seeing InitPHP\EventEmitter\EventEmitter not found, your autoloader is
probably not loading aliases.php — re-run composer dump-autoload.
Call Events::reset() in setUp() (and/or tearDown()). It drops the
shared singleton entirely — listeners, simulate / debug flags, and the
debug log all go with it; the next facade call rebuilds a fresh
Event instance.
final class MyTest extends \PHPUnit\Framework\TestCase
{
protected function setUp(): void
{
Events::reset();
}
protected function tearDown(): void
{
Events::reset();
}
}Other tools in the same family:
-
Events::setInstance($preconfigured)— inject a dispatcher with flags already set, instead of toggling them at the start of every test. -
Events::removeAllListeners()— drop listeners without dropping the flags/debug log. -
Events::clearDebug()— empty the debug log only.
If you would rather avoid the static facade entirely in tests,
instantiate new Event() (or new EventEmitter()) per test — neither
keeps any global state.
Open an issue with a minimal reproduction:
- github.com/InitPHP/Events/issues
- Include PHP version,
initphp/eventsversion, and a short snippet that demonstrates the unexpected behaviour.
initphp/events · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Core APIs
Practical
Reference