Cannot redeclare get_urls(): two trees, one function
| Symptom: 249 identical fatals | |
| Why require_once does not save you | |
| The two copies | |
| Fix: keep Rblock in its own tree | |
| Guard: belt and suspenders | |
| Verify: logs, render, deploy |
Symptom: 249 identical fatals
The dev error log filled with 249 copies of one line, all within minutes, referer /qa/robot/:
PHP Fatal error: Cannot redeclare get_urls() (previously declared in /.php/_modules/ads_dict.php:12) in /ao/.php/_modules/ads_dict.php on line 12
A companion case — live page truncated to 30 bytes — covered the same fatal from the symlink angle. This one is about the remaining angle: two engine trees defining one global function name.
Why require_once does not save you
require_once dedupes by resolved file path, not by content and not by function name. Two genuinely different files that happen to define get_urls() load twice, and the second definition fatals — exactly what happened here: /.php/_modules/ads_dict.php plus /ao/.php/_modules/ads_dict.php on one page.
No spelling trick helps; the dedup key (path) differs while the collision key (function name) is identical. Details and a path experiment: require vs include.
The two copies
House rule first: every page may only require modules from its own engine tree — all pages were already scanned for cross-engine calls and none was found. The ads dictionary still exists twice, once per tree, each declaring a global get_urls():
| /.php/_modules/ads_dict.php | guarded with function_exists |
| /ao/.php/_modules/ads_dict.php | guarded with function_exists |
Verifying “no page mixes engines” file-by-file misses it: not one file requires two dicts. The mixing happens one level down, in the runtime require graph — the log referer names the page:
/qa/robot/index.php:8 — require root /.php/_modules/ads_dict.php
→ defines get_urls() the first time — legal, page continues
/qa/robot/index.php:18 — boot the shared Page engine
Page::displayRblock() → Rblock.php:262
Rblock loads $root/ao/.php/right_blocks/robot.php (rnav: robot)
robot.php:13 — require ao dict → PHP meets a second
function get_urls() with the same name in the global scope;
require_once cannot stop it (different file path), so on any
build without the function_exists guard this line is fatal
The shared engine itself reaches into the ao tree: the ao right-block files (robot.php, qa.php, django.php, …) each require the ao dict — while ao content files symmetrically pull shared /.php/_modules/* helpers the other way. The engine boundary holds per file but not per request.
The same audit pattern applies to every shared helper: local_img() is guarded in both trees, img_from_dir() is guarded in both trees, but ao_img() is guarded in none of its four trees — latent today (no page loads two copies), a fatal the day one does.
Fix: keep Rblock in its own tree
The real fix removes the bridge instead of cushioning it: the shared Rblock must load right blocks from the shared /.php/right_blocks/ — never from /ao/.php/right_blocks/. The shared dir covers every ao file, so no page loses its ad block; pages simply stop executing a second engine tree per request. This also matches AdvertTwo, which already resolves $root/.php/right_blocks/$rnav.php.
$right_block_url = "$root/.php/right_blocks/$this->rnav.php";
Guard: belt and suspenders
The guard is not the fix — it is the safety net for the next bridge anyone builds. Wrap every global definition so a second load is a no-op:
if (!function_exists('get_urls')) {
function get_urls(...$numbers)
{ ... }
} // function_exists('get_urls')
Warning: the guard hides divergence. Whichever copy loads first wins silently, and the loser never runs — no error, no log line. Our two dicts match today, so either copy behaves the same; the day someone edits only one of them, pages start behaving differently depending on include order, and nothing will tell you. Treat a guard as a tripwire that bought time, not as permission to keep two copies: converge them into one file owned by one tree.
The guard is idempotent and version-proof: on PHP 4/5 the duplicate definition was an uncatchable compile fatal; since PHP 7 it throws a catchable Error — but uncaught it still kills the page, so the guard matters on every version.
Verify: logs, render, deploy
1. Render the previously fatal pages locally
(DOCUMENT_ROOT=container/src) and assert the
footer marker — the log names file and line outright, so it is always
the first place to look.
2. Old line numbers in the log (3 vs 11 vs 12) mean hosting runs older
code than the repo: after the fix, shadow-copy to dev, purge OPcache
plus recycle the PHP workers (upload alone is not deterministic —
stale workers were observed locally), then curl the failing URLs with
X-Forwarded-Proto: https and expect 200.
3. Re-download the logs a day later and re-run the analyzer on a
fresh window — the old report mixes rotated logs, so old fatals
linger and prove nothing.
Article author: Arthur Isaev
| Development with PHP | |
| PHP errors: case studies | |
| Top menu disappeared | |
| Live page truncated to 30 bytes | |
| Cannot redeclare get_urls() |