Most of the upgrade writing about PHP is still about the big jump — 5.6 or 7.4 to 8.0. That jump is real work, and we have written about the gotchas that actually bite. But a large share of the estates we look at now are not there anymore. They did the hard migration two or three years ago, landed on PHP 8.1, and stopped. That version left security support on 31 December 2025. A server running it today receives no upstream patches, and the next remote-code-execution advisory in the PHP core or a bundled extension will not have a fix you can apt upgrade into.
The good news is that this is a much smaller job than the last one. Here are the dates, the specific breakages, and the rollout.
The dates
| Version | Active support | Security support ends |
|---|---|---|
| 8.1 | ended Nov 2023 | 31 Dec 2025 — over |
| 8.2 | ended Dec 2024 | 31 Dec 2026 |
| 8.3 | ended Dec 2025 | 31 Dec 2027 |
| 8.4 | through Dec 2026 | 31 Dec 2028 |
| 8.5 | shipped Nov 2025 | 2029 |
Two sane targets. 8.3 is the low-friction hop: almost nothing was removed, and it buys you until the end of 2027. 8.4 costs one more round of deprecation cleanup and buys until the end of 2028. If your application has active development and a test suite, go to 8.4. If it is in maintenance mode and you want the cheapest defensible position, 8.3 is a perfectly honest answer — just diary the 2027 date now, because the whole reason you are reading this is that the last hop never got diarised.
Do not stop on 8.2. It has about a year left and costs the same effort as 8.3.
What actually breaks
8.2: dynamic properties
The one that generates real errors. Assigning to an undeclared property — $obj->whatever = 1 where whatever is not declared on the class — is deprecated, and it is everywhere in older code and older ORMs. In 8.2/8.3 it emits a deprecation notice; it is scheduled to become an error in a future major. Fix it properly by declaring the properties. Where you cannot (generated classes, a vendor base class), #[\AllowDynamicProperties] on the class is the sanctioned escape hatch. Classes extending stdClass and __get/__set users are unaffected.
8.2: ${} string interpolation
"Hello ${name}" and "${arr['k']}" are deprecated in favour of "Hello {$name}". Trivial, mechanical, and there are usually hundreds of them. grep -rn '\${' --include='*.php' finds them; Rector's PHP 8.2 set rewrites them.
8.2: utf8_encode() / utf8_decode()
Deprecated, and badly named to begin with — they only ever converted between Latin-1 and UTF-8. If the data really is Latin-1, mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1'). If you are not sure what the encoding is, that is a database question, not a PHP one, and the answer lives in your table collations.
8.4: implicitly nullable parameters
The main new noise on the way to 8.4. function f(Foo $x = null) is deprecated; write ?Foo $x = null. Old code and old libraries are full of it. Rector and PHP CS Fixer both handle it automatically, and the change is safe to apply blind.
8.4: smaller ones worth grepping
E_STRICT is deprecated (check your error_reporting lines and any php.ini copied forward since 5.4). session.sid_length and friends are deprecated. PDO_MySQL gained driver subclasses, which matters only if you inspect the PDO class name. Round out with get_class() and get_parent_class() called with no arguments, deprecated in 8.3.
The one that is not PHP's fault: extensions
Every compiled extension must be rebuilt for the new ABI — redis, imagick, xdebug, memcached, apcu, and anything from PECL. Distro packages handle this if you use them (deb.sury.org on Debian/Ubuntu, Remi on the RHEL family); hand-built extensions do not. And if any part of your codebase is encoded with ionCube or SourceGuardian, check their supported versions first: encoder support routinely trails a PHP release by months, and it has decided the target version for more than one client of ours.
Find the work before you start
Run these against the current codebase; they take an afternoon between them:
composer why-not php 8.4
vendor/bin/phpstan analyse --level=5
vendor/bin/rector process --dry-run
vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 8.4 src/
composer why-not is the one people skip and it is the most useful: it names the dependencies that block the target version, and usually the answer is a framework or two that need a minor bump first. Also check config.platform.php in composer.json — if it is pinned at 8.1.0, Composer has been resolving against a fiction and will keep doing so after you upgrade the server.
Then turn deprecations into a list instead of noise. In staging, set error_reporting = E_ALL, display_errors = Off, and error_log to a dedicated file, replay a day of real traffic, and sort the log. You will typically get five to fifteen distinct deprecation sites with a long tail of repeats — a bounded, countable amount of work rather than a vague risk.
Roll it out with a way back
The reason minor upgrades get postponed is that they feel like an all-or-nothing switch. They do not have to be:
- Install the new PHP alongside the old one. Both
suryand Remi supportphp8.1-fpmandphp8.4-fpmon the same host, listening on separate sockets. - Run CLI work first — cron jobs, queue workers, the deploy scripts — under the new binary. They fail cheaply and they exercise a surprising amount of code.
- Point one low-traffic vhost, or a single route, at the new FPM socket in Apache. Watch the error log and p95 latency for a day.
- Move the rest. Rollback is one line in the Apache config and a reload — the old pool is still running, warm, on the old socket.
- Remove the old packages only after a week of quiet. Not before; that is what makes the rollback real.
Expect a modest performance improvement, mostly from OPcache and JIT work across these releases. Measure it rather than quote it — for typical database-bound LAMP applications the win is real but small, because your time is going to MySQL, not to the interpreter.
When you do not need us
If you have a test suite, a staging environment, and composer why-not php 8.3 comes back clean, this is a day of work for your own developer and you should just do it. The engagements where we earn our hourly rate are the other ones: no tests, an encoded module, a framework three majors behind, or an application nobody currently employed has read end to end. If that is the shape of it, send us the versions and the symptoms and we will tell you which target version is realistic and what the compatibility inventory looks like before anyone touches a server.