The symptom had me second-guessing everything: on an editorial site I was porting from React to WordPress, there was a "dossier" section that was supposed to be rich with data from post meta, but what rendered was always the demo content. Forever. No error, no warning, not a single PHP notice in the log. The page rendered cleanly, returned a 200, the output was just wrong. And the most maddening part: it looked exactly like an importer bug, even though the importer was perfectly fine.
In React, the logic was this simple:
const value = deepMeta ?? fallback;If deepMeta exists, use it. If it's null or undefined, only then fall back. One line, unambiguous, no gap to slip through.
When I moved it to PHP, my hands wrote this instead:
$value = $deepMeta;
// ... a few lines in between ...
$value = $fallback;I reused the same $value for two different things: first for the decoded-meta candidate, then for the final value. And that second line ran with no condition at all.
Why this happens
The whole problem lives in the words "no condition." In React, ?? is an operator: it decides. $value = $fallback; in PHP is no operator at all — it's a plain assignment that executes every single time control reaches that line. So the real flow was:
$deepMeta = get_post_meta( $post_id, 'dossier_data', true ); // HAS a value
$value = $deepMeta; // good, $value now holds the real data
// ... other logic, formatting, escaping, etc ...
$value = $fallback; // BOOM - overwrites the real data, every time, unconditionallyBecause that last line wasn't wrapped in any condition, it wiped out the deep branch even when the meta was clearly present. The importer had correctly populated the post meta, the decode was correct, $deepMeta genuinely held the data — and then one innocent line below replaced all of it with the demo.
What made it slippery: nothing was syntactically wrong. PHP had no reason to complain. Overwriting a variable that already has a value is the most normal thing in the world. No undefined, no type error, no null. Just the right value, and a moment later, the wrong one. The bad output left no trace behind it.
I burned time because I suspected the wrong place. I tore into the importer, I var_dump-ed the result of get_post_meta, I checked whether the meta key was right. Everything came back green. $deepMeta was always populated when I dumped it. What I didn't notice was that I was dumping above the clobbering line, so all I proved was that the data arrived — not that it survived to the output.
The fix
The instant I dumped right before the echo, it was obvious: the value had already become the fallback. I scrolled up, and there was the unconditional $value = $fallback; staring back at me.
The fix is easy and should be a reflex: never use one variable for both "candidate" and "final." Give them distinct names, and only apply the fallback when the candidate is actually empty.
$deep_value = get_post_meta( $post_id, 'dossier_data', true );
if ( empty( $deep_value ) ) {
$value = $fallback;
} else {
$value = $deep_value;
}
echo $value;$deep_value holds the decoded-meta candidate, $value holds the final value, and the fallback assignment is now guarded by empty(). The fallback only runs when there's genuinely no deep data — exactly like the original ?? in React. Once I deployed, the dossier section immediately rendered the real data that had been sitting in the database all along.
If you want it tighter, modern PHP's ternary keeps the spirit of that React line in a single expression:
$value = ! empty( $deep_value ) ? $deep_value : $fallback;The style isn't the point; the principle is. The "which one do we use" decision has to live in one place, not get scattered across two separate assignments where one of them runs unconditionally.
The takeaway
The most expensive bugs are often the quietest. No stack trace to lead you, no red line in the log — just wrong output that looks plausible, right up until you actually compare it against what it should be.
The concrete lesson I walked away with: never reuse one variable for both the "candidate" and "final" roles. A later unconditional assignment is a silent clobber, and no error will ever reveal it. Operators like ?? in JavaScript fuse the decision and the assignment into one thing, so it's impossible to "forget" the condition. The moment you translate that into two separate lines in PHP, that condition becomes your responsibility, and it's terribly easy to drop.
And one debugging habit: when the output is wrong but the source data is right, dump the value right before it's used, not right after it's fetched. The gap between "data arrived" and "data used" is exactly where the clobber hides.
