For years I had one habit that felt like enough: before shipping any PHP file to a server, run it through php -l. Clean output meant safe to upload. That habit survived until an afternoon when every single file I shipped passed the lint check, and the site still went completely dark the moment the upload finished.
The symptom: one upload, the entire site gone
I was working on a custom theme for a WordPress site built around a listing directory. The day's changes touched several files inside inc/, so I uploaded that whole folder at once. Before shipping, I checked each file:
php -l inc/directory-ui.php
# No syntax errors detected in inc/directory-ui.phpAll clean. Upload went through. Then the site vanished.
Not one broken page, not one misbehaving template. Both the public pages and /wp-admin served a blank white screen with "There has been a critical error on this website". There was no back door through the dashboard, because the dashboard was dead too.
The error was already waiting in the log, roughly like this:
PHP Fatal error: Cannot redeclare theme_geo_label() (previously declared in
/wp-content/themes/custom-theme/inc/template-functions.php) in
/wp-content/themes/custom-theme/inc/directory-ui.phpIt threw me for a few seconds, because that message named two files I had just personally declared clean.
Why php -l cannot catch this
This is the part I already knew in theory but had never actually felt the consequences of.
php -l does exactly one thing: it parses a single file and tells you whether its contents are valid PHP. It does not execute anything, it does not follow require, and it has no idea any other file in the theme exists.
That is where the trap sits. Both of my files were syntactically correct. inc/template-functions.php was valid. inc/directory-ui.php was valid. No unclosed brace, no missing semicolon. A duplicate function declaration is not a syntax error at all. It is a runtime error that only comes into existence the instant both definitions land in the same PHP process.
The honest framing: per-file linting answers "is this file valid?", never "is this file safe to combine with the dozens of other files in this theme?". I had been treating the answer to the first question as though it settled the second.
The root cause: twin functions that always load together
I had added a new helper called theme_geo_label() in inc/directory-ui.php. That exact name already existed in inc/template-functions.php, written a long time ago for something unrelated in a different part of the theme.
The collision was as plain as this:
// inc/template-functions.php (already there, long before me)
function theme_geo_label( $post_id ) {
// ...
}
// inc/directory-ui.php (my new helper)
function theme_geo_label( $post_id ) {
// ...
}What turned it into a total outage rather than one broken page was how both files load. They are both required from functions.php with no conditions attached, which means both are read on every request, frontend and admin alike. No request path escaped it, so no page survived. That explains why /wp-admin went down with everything else, and why the whole thing felt far more urgent than an ordinary bug.
One thing I deliberately did not do: wrap the new declaration in function_exists() to silence the error. That would remove the fatal, but the result is worse. Whichever definition happens to load first wins, and the bug mutates from a loud crash into quietly wrong behaviour. Loud is cheaper.
The fix: rename one function, upload one file
Recovery turned out to be far smaller than the panic surrounding it. I renamed the new helper to something specific enough that it cannot collide, then re-uploaded that single file:
// inc/directory-ui.php
function theme_post_geo_label( $post_id ) {
// ...
}One file up, site back within seconds. Total downtime was short, but for those few minutes the site was genuinely at zero, admin entrance included.
The gate I now run before every delivery
Since php -l is structurally incapable of helping here, I added a manual step before shipping: for every new function, grep the whole theme and confirm the name appears in exactly one file.
grep -rl "function theme_geo_label(" --include='*.php' .
# must return exactly 1 fileTwo lines of output means stop. Do not upload yet.
For files that introduce several functions at once, I run the loop version so nothing slips through:
grep -oE "^function [a-zA-Z0-9_]+" inc/directory-ui.php | awk '{print $2}' | while read fn; do
n=$(grep -rl "function $fn(" --include='*.php' . | wc -l | tr -d ' ')
echo "$n $fn"
doneEvery line has to start with 1. A 2 or higher is a collision waiting to detonate on the server rather than on your laptop.
What I took away
php -lvalidates one file, not a combination of files. Passing lint is not the same as being safe to deploy.Cannot redeclareis a class of error that can only appear at runtime, when two definitions meet in the same process. No per-file tool can predict it.- Files
required unconditionally fromfunctions.phpturn any fatal into a whole-site fatal, admin included. That makes a name collision there far more expensive than one inside a conditionally loaded file. - Long, specific function names are cheap. Reviving a dead site at the wrong hour is not.
- The gate is a single
grep. Running it costs seconds, skipping it cost me the entire site.