Upgrading from PHP 8.2 to 8.4: what breaks and how to fix it

By Ryan Murdoc, founder of Unstale · Published 13 October 2026 · Last checked October 13, 2026 · 5 min read

PHP 8.2 security support ends December 31, 2026.

PHP 8.2 stops getting security fixes on December 31, 2026. After that, new vulnerabilities in 8.2 won't be patched, and hosting providers start retiring it. PHP 8.4 is supported until the end of 2028.

The jump goes through 8.3, but you don't need to stop there: upgrade straight to 8.4 and fix what both releases changed. For most apps the language changes are small. The real work is dependencies and a long tail of deprecation warnings.

Before you start

  1. Get the test suite passing on 8.2 today. You need a known-good baseline. If a test fails before you change anything, write it down so you don't chase it later.
  2. Make deprecations visible. Run tests with error_reporting=E_ALL and display_errors=1, or set PHPUnit's failOnDeprecation="true". Most PHP 8.4 work shows up as deprecations, not errors.
  3. Check your framework. Laravel 11 supports PHP 8.2–8.4; Laravel 12 supports 8.2–8.5; Laravel 13 needs 8.3 or newer. If you're on Laravel 10, upgrade the framework first: Laravel 10 does not support PHP 8.4.
  4. Tell Composer the target. Set "config": {"platform": {"php": "8.4.0"}} in composer.json and run composer update. Composer will show every package that blocks 8.4.

1. Implicitly nullable parameters (the big one)

PHP 8.4 deprecates giving a typed parameter a null default without marking the type nullable. Older codebases often have hundreds of these.

// Before: deprecated in 8.4
function save(User $user = null) { ... }

// After
function save(?User $user = null) { ... }

If a parameter like this sits before a required one, drop the default too: function f(A $a, ?B $b, C $c).

How to fix it: don't do it by hand. PHP-CS-Fixer's nullable_type_declaration_for_default_null_value rule, or Rector's ExplicitNullableParamTypeRector, rewrites them all. Review the diff, then run the tests.

2. exit() and die() are now functions

In 8.4, exit() behaves like a function. It respects strict_types and throws a TypeError for arguments that aren't an integer or string, where it used to cast silently.

exit($result);        // TypeError in 8.4 if $result is an array, null or bool
exit((int) $result);  // explicit

How to find it: search for exit( and die( with a variable argument.

3. Functions that now throw instead of guessing

Several functions used to accept bad arguments and carry on. In 8.4 they throw a ValueError:

  • round() with an invalid rounding mode (it used to round half-up silently).
  • str_getcsv() when the separator or enclosure isn't exactly one byte.
  • unserialize() when allowed_classes isn't an array of class names or a bool.

PHP 8.3 did the same to range(): objects, arrays, a zero step or a negative step on an increasing range now throw.

4. Behaviour changes that tests can miss

These don't throw. They return different results, so only a test that checks the exact output will notice.

  • Traits with static properties (8.3). Each class using the trait now gets its own copy of the static property. A counter or registry kept in a trait used by several classes will split.
  • number_format() with negative decimals (8.3) now rounds to the left of the decimal point instead of ignoring the value.
  • Regular expressions (8.4). The bundled PCRE2 library was updated, and {,3} is now a quantifier (zero to three), not literal text. Search your patterns for {,.
  • Dates (8.3). The Date extension throws DateException and DateError subclasses where it used to emit warnings. Code that checked for false after a warning may now see an exception.

5. Other deprecations to search for

Each of these logs a deprecation in 8.4. None are hard to fix; the trick is finding them all.

Search for Replace with
trigger_error(..., E_USER_ERROR) throw an exception, or exit
E_STRICT remove it (the level no longer exists)
0 ** -1, pow(0, -n) fpow() if you want IEEE behaviour
fgetcsv, fputcsv, str_getcsv without escape pass escape explicitly
lcg_value() Random\Randomizer::getFloat()
mysqli_ping, mysqli_kill, mysqli_refresh remove, or use SQL KILL / FLUSH
new ReflectionMethod('Class::method') ReflectionMethod::createFromMethodName()
get_class() / get_parent_class() with no argument (8.3) self::class / parent::class
++ on an empty or non-alphanumeric string (8.3) str_increment() or explicit logic

6. Name clashes with new functions

PHP 8.4 adds global functions including array_find(), array_find_key(), array_any(), array_all(), mb_trim() and fpow(). If your code or an old helper package defines a function with the same name without a function_exists() guard, PHP stops with a redeclaration error. Search for function array_find and friends.

7. Server and extension changes

  • Extensions removed from core: IMAP, OCI8, PDO_OCI and PSpell moved to PECL in 8.4. If you use imap_* functions, plan a replacement or install the PECL package.
  • JIT: 8.4 disables JIT with opcache.jit=disable by default. If you turned JIT on only by setting opcache.jit_buffer_size, also set opcache.jit (for example tracing).
  • Sessions: changing session.sid_length, session.sid_bits_per_character, session.use_trans_sid and session.use_only_cookies is deprecated.
  • MySQL: server wait timeouts now report error 4031 instead of 2006 on MySQL 8.0.24+. Reconnect logic that checks for 2006 needs both.

Upgrade checklist

  • Tests pass on 8.2 with deprecations visible
  • Framework on a version that supports 8.4
  • composer update resolves with the platform set to 8.4
  • Implicitly nullable parameters fixed with a tool, diff reviewed
  • Every search in sections 2–6 run, hits fixed
  • Tests pass on 8.4 with zero new deprecations
  • Docker image, CI, .php-version and hosting runtime updated
  • Staging deploy checked: main pages, a form submit, a queued job, a scheduled task

Sources