I was assembling a new plugin — six classes, a loader, the usual structure. It installed. It activated. No errors, no warnings, nothing in the log.
It also did absolutely nothing. No admin menu, no hooks registered, no classes defined. As far as WordPress was concerned the plugin was active and healthy. As far as the plugin was concerned, it had never run.
The cause is a PHP behavior I knew about in the abstract and had never once been bitten by, and the failure mode is silent in a way that makes it worth writing down.
The code
Standard defensive pattern. Bail out if the plugin has somehow been loaded twice:
if ( function_exists( 'csmonhub_fs' ) ) {
return;
}
function csmonhub_fs() {
// Freemius init
}
require_once __DIR__ . '/includes/class-csmonhub-crypto.php';
require_once __DIR__ . '/includes/class-csmonhub-hub.php';
// ... four more requires, hook registrations, everything else
Read it top to bottom and it’s obviously fine. First load: the function doesn’t exist yet, the guard doesn’t fire, the function gets declared, everything below runs. Second load: the function exists, we return early, no fatal redeclaration error.
That reasoning is wrong, and it’s wrong on the very first line.
What PHP actually does
A function declared unconditionally at the top level of a file is defined at compile time — before a single line of that file executes.
PHP compiles the whole file first. Top-level function declarations get registered during that pass. Only then does execution begin at line one.
So by the time PHP evaluates if ( function_exists( 'csmonhub_fs' ) ), the answer is already yes. Not because the file ran before. Because the file was compiled, and compiling it defined the function.
The guard fires on the first load. The return executes. Every require_once below it, every class definition, every add_action — none of it runs.
And nothing errors, because nothing is wrong. A file that returns early is a perfectly valid file. WordPress asked PHP to include the plugin, PHP included it, PHP came back. Activation succeeds. The plugins list shows it as active. There is no symptom other than complete absence of behavior.
The distinction that matters
Not all function declarations are hoisted. The rule is about position:
// Hoisted — defined at compile time, before any execution
function foo() {}
// NOT hoisted — defined at runtime, only when execution reaches it
if ( $condition ) {
function bar() {}
}
A declaration inside a conditional, a loop, or another function is a runtime declaration. It doesn’t exist until execution arrives at it.
Which means the fix is to make the declaration conditional — the exact pattern WordPress plugins have been using for years, and which I’d have copied if I’d copied it rather than writing a guard from scratch:
if ( ! function_exists( 'csmonhub_fs' ) ) {
function csmonhub_fs() {
// Freemius init
}
require_once __DIR__ . '/includes/class-csmonhub-crypto.php';
require_once __DIR__ . '/includes/class-csmonhub-hub.php';
// ... everything else
}
Now the declaration is inside a conditional, so it isn’t hoisted. On the first load function_exists() is genuinely false, the block runs, and the function is defined at the moment execution reaches it. On any subsequent load it’s true and the block is skipped.
Same intent. The difference is entirely in whether the declaration sits at the top level of the file or inside a block.
Why it took a while to find
Because there was nothing to find. My instinct with a plugin that isn’t working is to look at the log, and the log was clean. Then to check whether the file is being loaded at all — it was. Then to check for a fatal partway through — there wasn’t one.
The thing that actually found it was a test harness: a stub environment that loads the plugin and asserts that each expected class exists and each expected hook is registered. It reported that zero of six classes had been defined, which is a much more useful message than “the plugin doesn’t work,” and it pointed straight at the loader.
That’s the real lesson. A guard clause that fires when it shouldn’t produces silence, and silence is the hardest failure to debug. An exception has a stack trace. A fatal has a line number. An early return has nothing at all — it looks identical to a plugin that simply has nothing to say.
Two habits I’ve taken from it:
Assert on load. Even a crude check — does the main class exist after the plugin file is included — turns an invisible failure into an obvious one.
Be suspicious of guard clauses you wrote yourself. The if ( ! function_exists() ) wrapper is boilerplate in half the plugins on WordPress.org, and there’s a reason it’s shaped exactly that way. I “improved” it into an early return because that read more cleanly, and in doing so I moved the declaration to the top level of the file, which is the entire bug.
The one-line version
If you have a guard that checks for a function declared later in the same file, at the top level, the guard will always fire. Wrap the declaration in the conditional instead of returning early.
Canary Site Monitor is free on WordPress.org.