D
P
0
← All articles Baca dalam Bahasa Indonesia

WordPress & PHP in Production

`Cannot redeclare theme_geo_label()`: php -l Said Clean, Uploading `inc/` Took the Whole Site Down

· · 5 min read
`Cannot redeclare theme_geo_label()`: php -l Said Clean, Uploading `inc/` Took the Whole Site Down

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.php

All 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.php

It 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 file

Two 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"
done

Every 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