Someone finally did the homework the rest of us keep skipping: they built the VLD extension against PHP 8.3.6 and dumped the compiled opcodes of the same function twice, once with declare(strict_types=1) at the top of the file and once without. The two listings match. Same RECV pulling in the argument, same VERIFY_RETURN_TYPE on the way out, identical structure from first opcode to last. Strictness never makes it into the bytecode. It lives as a flag on the compiled file, and the runtime consults that flag inside a type check the engine performs anyway.

vld: greet(int $age): string
Function greet:
line #* E I O op fetch ext return operands
 2 0 E > RECV !0
 3 1 ROPE_INIT 3 ~2 'You+are+'
 2 ROPE_ADD 1 ~2 ~2, !0
 3 ROPE_END 2 ~1 ~2, '+years+old'
 4 VERIFY_RETURN_TYPE ~1
 5 > RETURN ~1

That one measurement quietly kills the performance objection. The comparison between declared type and actual value happens on every typed call, strict or not; the flag only decides whether a mismatch throws a TypeError or triggers a conversion attempt. Throwing is, if anything, the cheaper branch. So cost was never the real question. The real question is topology, because the flag belongs to the file where the call originates, and that detail is where teams get hurt. Here is where I land: full adoption or none. A half-strict codebase is worse than a fully loose one, because at least the loose one is predictably wrong.

Walk through what per-file actually means. For arguments, the caller's file decides: if file A declares strict_types and calls a takesInt() defined in a loose file B, that call is strict. For return values it flips, the check runs where the return statement lives, so a function in a loose file can happily coerce the string "5" into the int it promised no matter how strict its caller is. Internally that is consistent, the check obeys the file it executes in. From the outside it means the exact same function accepts "25" when called from admin/report.php and throws when called from cron/rebuild.php. Try explaining that to whoever is on call when those two code paths disagree at 2 a.m.

And loose mode's failure mode is not loud, it is arithmetic. Hand a date string like "2024-01-01" to a parameter declared int and PHP keeps the leading digits and drops the rest, so your function is now computing with 2024. Two dates in the same year collapse to the same number, a day count comes out as zero, a report renders empty, and nothing warns you. The write-up that ran the VLD experiment traced exactly this pattern through a financial reporting service, and the debugging bill was two days. With the flag set, the same mistake is a TypeError with a stack trace pointing at the caller, fixed in under a minute.

The honest counter-argument deserves airtime. Legacy code can depend on coercion on purpose: form input arrives as strings, old config loaders hand you "1" where a bool is meant, and a big-bang flip surfaces every tolerated mismatch at once, in production if you are careless. There are also real limits to what the flag buys you. Internal functions ignore it completely, strlen(123) coerces in the strictest file you own. Class types were strict all along, same for arrays and callables. And a library author cannot impose strictness on consumers, because the caller's file wins. So no, strict_types is not a force field. It governs scalar declarations on your own functions, nothing more.

I still push for universal adoption, precisely because the scope is that narrow and the cost is that close to zero. Declared types that are actually enforced are the ground PHPStan and Psalm stand on; without enforcement, your signatures are optimistic comments. The migration is boring but tractable. New files get the declare line without discussion. Old files get it one at a time with the test suite running. The TypeErrors that surface get fixed at the real boundary, meaning one explicit (int) cast where request data enters the system, not casts sprinkled across every call site. If you find yourself casting at every caller, the signature is lying anyway and probably wants a union type instead.

One thing the opcode dump left me chewing on: if strictness is just a different branch at runtime, then per-file opt-in was a language design choice, not a technical necessity. PHP could have made it engine-wide and chose not to, presumably so twenty years of existing code keeps running, which is fair. But that choice pushes the consistency burden onto us. So tell me how you carry it. Is your codebase at 100 percent strict_types, and if not, which file will never get the declare line, and what is the honest reason why? I suspect the answers say more about our systems than any bytecode dump does.