Upgrade from PHP 8.2 to 8.4: patch now, then make the jump

PHP 8.2 reaches end of life on 31 Dec 2026. Install 8.2.35 for the PHP2Shell fix today, then move to 8.4 using the 8.3 and 8.4 change lists and scan commands.

Two jobs, in this order

PHP 8.2 gets security fixes until 31 Dec 2026, about ten weeks from today. After that there are no more official releases for it. See PHP 8.2.

  1. Today: install 8.2.35. It carries the PHP2Shell fixes for CVE-2026-109618 and CVE-2026-109619. Restart every PHP worker and confirm the running version.
  2. Before the end of the year: move to 8.4 (fixed in 8.4.27). It gets security fixes until 31 Dec 2028. Which version to pick has the comparison.

Do the first job even if the second takes weeks. A patch release inside a branch is a small change. The minor-version jump needs staging and tests.

If your distribution or host has not shipped 8.2.35, the package guides under fix say what each vendor has released. On shared hosting, ask the host and see shared hosting.

Order of work for the jump

  1. Back up files and database. Test a restore.
  2. Build a staging copy on 8.4 with the same extensions and php.ini values.
  3. Update CMS, framework, plugins and Composer packages to versions that declare 8.4 support. composer outdated and composer why-not php 8.4 show what blocks you.
  4. Run the compatibility scan below.
  5. Run staging with error_reporting = E_ALL and log_errors = On. Test logins, forms, uploads, checkout and scheduled jobs. Read the log.
  6. Switch production, restart every PHP worker and confirm the version.
  7. Keep 8.2 one panel switch away for a few days.

Your 8.2 logs already show the 8.2 deprecations, mainly dynamic properties and utf8_encode(). Fix those first, since they are still deprecated on 8.4.

PHP 8.3 changes

8.3 breaks little. The items below are the ones that come up in real projects.

  • range() is stricter. A step of 0, a negative step on an increasing range, and infinity or NAN throw ValueError. Mixed calls such as range(5, 'z') emit a warning.
  • number_format() with a negative $decimals now rounds before the decimal point instead of ignoring it.
  • Negative array index. After $a[-5] = 'x' on an empty array, the next $a[] gets -4, not 0.
  • Date errors now use DateError and DateException classes instead of warnings. Check code that wraps date parsing in try/catch.
  • Deep recursion can throw an Error when the call stack nears its limit.
  • Deprecated: get_class() and get_parent_class() without arguments, assert_options() with the assert.* INI settings, ReflectionProperty::setValue() with one argument, and the MT_RAND_PHP mode.
  • Incrementing non-numeric strings is soft deprecated. Use str_increment() in new code.
  • Removed: the opcache.consistency_checks INI directive. Delete it from php.ini to avoid startup noise.

PHP 8.4 changes

  • Implicitly nullable parameters. function f(Foo $a = null) is deprecated. Write ?Foo $a = null. Outdated libraries trigger this on every request. Update packages before you judge your own code.
  • E_STRICT is removed as an error level. trigger_error() with E_USER_ERROR is deprecated, so throw an exception.
  • CSV functions. Relying on the default escape argument of fgetcsv(), fputcsv() and str_getcsv() is deprecated. Pass escape explicitly.
  • Moved to PECL: IMAP, OCI8, PDO_OCI and PSpell. If you call imap_* functions, install the PECL package or change libraries.
  • Session settings. Changing session.sid_length, session.sid_bits_per_character, session.use_only_cookies, session.use_trans_sid, session.trans_sid_tags, session.trans_sid_hosts or session.referer_check is deprecated. So is the SID constant.
  • mysqli: mysqli_ping(), mysqli_kill() and mysqli_refresh() are deprecated.
  • round() throws ValueError for an invalid mode, and the old pre-rounding was removed, so a few edge results change.
  • exit() acts like a function. It respects strict_types and throws TypeError for an invalid type.
  • JIT defaults changed. If you enabled JIT only through opcache.jit_buffer_size, set opcache.jit too.
  • bcrypt cost defaults to 12 instead of 10. Logins take longer. Existing hashes still verify.
  • Name collisions. A function or class added in 8.4 breaks code that defines the same name. Guard your own helpers with function_exists().
  • Minimum libraries: libcurl 7.61.0, libxml2 2.9.4 and OpenSSL 1.1.1. Check older servers. Apache 2.4 is the minimum for mod_php.

Scan and fix commands

Run these on a workstation or in CI.

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -i

vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.2-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

The range 8.2-8.4 reports anything that affects either version, so the code is safe while both run. After cutover, use --runtime-set testVersion 8.4. The ^10.0.0@dev constraint is the one in the PHPCompatibility README on 22 Oct 2026. It needs PHP_CodeSniffer 4.0.2 or newer.

Rector can fix some of the findings, such as explicit nullable types. Review the diff before you commit.

composer require rector/rector --dev
vendor/bin/rector process --dry-run
vendor/bin/rector process

Use return RectorConfig::configure()->withPaths([__DIR__ . '/src'])->withPhpSets(); in rector.php. Rector then reads the PHP version from composer.json.

Never rewrite code in plugins or themes you did not write. Update them. For WordPress, see change the PHP version.

Last reviewed on 22 October 2026.