register_globals and the whole catastrophe
I have written PHP since 1999, on version 4. Twenty-seven years of watching confident practice become embarrassing is a specific kind of training.
5 min read
I've written PHP since 1999, on version 4. I am currently on PHP 8.4 and Laravel 13, and I have been on Laravel since the version 4 beta. Twenty-seven years of watching confident practice become embarrassing is a specific kind of training. It left me less interested in certainty and more interested in the judgment behind a decision. This post is the receipt.
Code often outlives the confidence with which it was written. After shipping through several generations of PHP best practice that later became a finding, I trust current consensus less than I used to. Not because consensus is always wrong. Because I have watched it be locally right, then expensive, then a joke, without the people inside it noticing the cost had moved.
The catastrophe, itemized
register_globals was on by default before PHP 4.2. Incoming request data became global variables in the script. It felt like a gift: you named a form field and the value was just there. magic_quotes_gpc was the matching courtesy, escaping GET, POST, and cookie data so you would not have to think about it.
I wrote production PHP under those defaults, and I was not the person in the room explaining why they were a trap. They were the field's confident practice, and the reasoning was locally correct. PHP was trying to be the language a person could learn on cheap hosting without a computer science degree. Convenience was the product. If the language did not meet you halfway, you used something else.
Then the consensus flipped. register_globals went off by default in 4.2 and was removed in 5.4. Magic quotes went with it. Code that had been normal became a vulnerability report. Not because we had been stupid. Because the cost of being sloppy had become visible, and the language stopped carrying the old habit.
I watched that flip as a working PHP 4 developer, not as a historian. The embarrassing part is not that the defaults were wrong in hindsight. The embarrassing part is how complete they felt at the time. You did not experience them as a gamble. You experienced them as how PHP worked.
The pattern that repeats
A practice is correct under a cost structure. The cost structure moves. The practice becomes a joke, and the people who still remember why it was correct sound like they are making excuses.
Laravel is the long example I actually have, version 4 beta through 13. That is a lot of years of "this is how we do it" turning into "we do not do it that way anymore," without throwing the career out and starting over. The never-rewrite rule sat underneath a lot of it. Don't start over. Migrate. Keep the system that is making money. The rule was economic, not moral: a rewrite cost a year of salaries and usually failed. "Never rewrite" was a cost structure wearing a principle's clothes.
We did rewrite once when I was in the chair. At Life Happens I led a ground-up rebuild of the agent-facing Laravel platform, driven by SEC compliance, event-sourced so any account's complete state could be reconstructed at any point in time. The never-rewrite rule bent when the constraint was reconstructability, not elegance. The rule held until the underlying cost changed, and then it looked obvious that we should have done the thing the rule forbade. People who only saw the second cost structure will think the first group was timid. People who only saw the first will think the second group was reckless. Both groups are describing a price.
Legacy code is mostly that record: code that outlived the assumptions it was written under. Confidence is not a property of the code. It is a property of the moment.
Applying it to now
A lot of current certainty is cost-dependent rather than principled. Generated code is cheap, so we talk as if volume is free and verification will catch up. "Don't rewrite" is still recited as morals. Meanwhile we are stocking repositories with comprehension debt, code that exists in git and not in anyone's head, and paying that down still requires someone to read at human speed. Generation did not change that speed.
I do not know which current practice will be the next register_globals. If I named one with confidence I would be repeating the original mistake. The candidates are obvious enough to be uninteresting: trusting tests we did not read, treating a generated diff as reviewed because it compiled, keeping the never-rewrite rule after the rewrite got cheaper, throwing away the junior work that used to produce seniors. Listing them is not the same as having judged them.
What will I be embarrassed about in 2038? I don't know. If I could see it from here I would already be hedging, and a confident prediction would be a costume of the same error.
Why this is worth saying out loud
The field is currently full of people who have only seen one cost structure. They are not wrong about the structure they are in. They are under-equipped for the next flip, because a flip does not announce itself as a debate. It announces itself as a default that later gets removed.
Longevity is not wisdom. It is a dataset. Twenty-seven years of PHP is not an argument that I am right now. It is a record of having been right in a way that later became indefensible, in public, in production, with the language itself as the witness. I assume at least one of my current certainties is the same kind of locally correct mistake. I will not know which one until it is already a little late.
Related: Legacy code, Comprehension debt, Hard deadlines