PHP 8 Migration: The Gotchas That Actually Bite

PHP 7.4 left security support in November 2022; PHP 8.0 followed in 2023. If your application still runs on either, every new CVE is a permanent exposure. The migration to PHP 8.x is usually less work than teams fear — but the work that does exist hides in specific, predictable places. This is the list we check first, in the order it tends to bite.

The type-juggling change that breaks quietly

PHP 8 changed how a string compares to a number. In PHP 7, 0 == "foo" was true, because the string was coerced to a number. In PHP 8, when one side is a numeric string and the other is a number, the comparison is numeric; when the string is non-numeric, the number is cast to a string instead. 0 == "foo" is now false.

This is the most dangerous change in the whole migration because nothing warns you. No deprecation notice, no fatal error — a conditional simply starts taking the other branch. Grep for loose comparisons against user input, status fields, and anything read from the database as a string. Where the intent is "is this the sentinel value", replace == with === and fix the types.

Internal functions now throw

In PHP 7, passing garbage to an internal function usually got you a warning and a null return, and the code stumbled on. In PHP 8, most internal functions throw a TypeError or ValueError instead. strlen(null) is deprecated; array_key_exists on a non-array is fatal.

The pattern that suffers most is old code that trusts $_GET/$_POST shapes: a missing key produces null, the null flows three calls deep, and what used to be a warning in a log nobody read is now a 500. The fix is boring and healthy: validate at the boundary, use null coalescing ($_GET['id'] ?? null) deliberately, and stop suppressing warnings with @.

Removals worth grepping for

  • each() — gone since 8.0. Rewrite the while (list(...) = each(...)) loops as foreach.
  • create_function() — gone. Replace with real closures.
  • Curly-brace string offsets ($str{0}) — gone. Use square brackets.
  • get_magic_quotes_gpc() and friends — gone entirely.
  • Dynamic properties on plain classes are deprecated in 8.2. Legacy code that hangs ad-hoc properties off stdClass-ish objects should either declare the properties or mark the class #[\AllowDynamicProperties] while you fix it.

Inventory before you touch anything

Do not migrate by deploying and watching the error log. Two tools give you a compatibility inventory up front:

  • PHP_CodeSniffer with the PHPCompatibility ruleset scans the codebase against a target version and lists every incompatibility with file and line. Run it against 8.1 or 8.2, not just 8.0, so you only do this once.
  • Rector can mechanically apply a large share of the fixes — signature changes, each() rewrites, string-offset syntax — leaving humans the semantic cases like loose comparisons.

Check extensions at the same time. mysql_* functions died with PHP 7; if you are coming from 5.x you have a mysqli/PDO rewrite in scope. Pinned PECL extensions (imagick, redis, memcached) need versions built for your target PHP.

Scaffold tests where none exist

Most applications this age do not have a test suite, and you do not need full coverage to migrate safely. What you need is a smoke harness: a script that hits the twenty routes that matter — login, search, checkout, the admin screens, the cron entry points — and asserts on status codes and a content fingerprint. An afternoon of work, and it turns "deploy and pray" into "deploy and diff".

Capture a baseline on the current PHP version first, then run the same harness against 8.x in staging. Differences are your work list.

Roll out in stages

Run both PHP versions side by side — two PHP-FPM pools, or a second server behind the load balancer — and move traffic incrementally. Start with internal users, then a small percentage of production. Keep the old pool warm for a fast rollback during the first week. Watch the error log at E_DEPRECATED level even after cutover: deprecations are the work list for the next upgrade, and they are much cheaper to fix while the migration context is fresh.

The whole exercise typically lands between a few days and a few weeks of engineering, depending on codebase size and how much of it predates PHP 7. That is a small price for getting back onto a supported runtime — and PHP 8 usually pays some of it back immediately, since real-world request throughput commonly improves without any tuning at all.