Upgrade from PHP 8.1 to 8.4: what to fix and in what order

PHP 8.1 has no official PHP2Shell release. This guide covers the 8.2, 8.3 and 8.4 changes that break real sites, plus the scan commands and a safe order.

Where you stand

PHP 8.1 reached end of life on 31 Dec 2025. For PHP2Shell (CVE-2026-109618 and CVE-2026-109619) the PHP project published only a source backport branch for 8.1. There is no 8.1 release with the fixes. The fixed releases are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Moving up is the supported fix. See PHP 8.1 and, if you cannot move yet, what to do meanwhile.

The target is 8.4 (fixed in 8.4.27). Which version to pick explains why not 8.2.

If you are coming from 7.4 or 8.0, use the 7.4 to 8.4 guide, which also covers the 8.0 and 8.1 changes.

Order of work

  1. Back up files and database, and test a restore.
  2. Build a staging copy on 8.4 with the same extensions and php.ini values.
  3. Update your CMS, framework, plugins and Composer packages to versions that declare 8.4 support. composer why-not php 8.4 lists the blockers.
  4. Run the compatibility scan below.
  5. Fix findings from the 8.2 list first. Dynamic properties and utf8_encode() are the usual offenders.
  6. Run staging with error_reporting = E_ALL and log_errors = On. Exercise logins, forms, uploads, checkout and cron jobs. Read the log.
  7. Switch production, restart every PHP worker and confirm the running version.

Going straight from 8.1 to 8.4 is fine. Treat the three version sections below as one checklist.

PHP 8.2

  • Dynamic properties are deprecated. Assigning to a property the class never declared logs a deprecation. Declare the property. As a stopgap, add #[\AllowDynamicProperties] to the class. stdClass and classes with __get() and __set() are not affected. Old plugins are the most common source.
  • "${var}" and "${expr}" interpolation is deprecated. Use "{$var}" or "$var".
  • utf8_encode() and utf8_decode() are deprecated. Use mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') or iconv().
  • Relative callables such as "self::method" and ["parent", "method"] are deprecated. Use [self::class, 'method'] or self::method(...).
  • mbstring encodings. Base64, QPrint, Uuencode and HTML-ENTITIES as mbstring encodings are deprecated. Use base64_encode(), quoted_printable_encode(), convert_uuencode() and htmlentities().
  • Case functions ignore the locale. strtolower(), strtoupper(), ucfirst(), ucwords() and similar now work as in the "C" locale. For accented or non-Latin text, use the mb_* versions.
  • Smaller changes. str_split('') returns [] instead of [""]. var_export() puts a leading backslash on class names. glob() returns an empty array instead of false when open_basedir blocks every path.

PHP 8.3

8.3 breaks little, which is why it is a fair fallback target.

  • range() is stricter. A step of 0, a negative step on an increasing range, and infinity or NAN throw ValueError. Some mixed-type calls such as range(5, 'z') now emit a warning.
  • number_format() with a negative $decimals now rounds before the decimal point instead of ignoring the value.
  • Array keys. After assigning a negative index to an empty array, the next $a[] uses that index plus one, not 0.
  • Date errors use the new DateError and DateException classes instead of warnings.
  • Stack overflow detection. Very deep recursion can now throw an Error before the process crashes.
  • Deprecated: get_class() and get_parent_class() with no arguments, assert_options() and the assert.* INI settings, ReflectionProperty::setValue() with a single argument, ldap_connect() with separate host and port, and the MT_RAND_PHP mode.
  • Incrementing non-numeric strings such as $s = 'a'; $s++ is soft deprecated. New code should call str_increment().
  • Removed INI setting: opcache.consistency_checks.

PHP 8.4

  • Implicitly nullable parameters. function f(Foo $a = null) is deprecated. Write ?Foo $a = null. Outdated libraries trigger this on every request, so update packages before judging your own code.
  • E_STRICT is removed as an error level, and trigger_error() with E_USER_ERROR is deprecated. Throw an exception instead.
  • CSV functions. Relying on the default escape argument of fgetcsv(), fputcsv() and str_getcsv() is deprecated. Pass it explicitly. str_getcsv() also throws if separator or enclosure is not one byte.
  • Moved to PECL: IMAP, OCI8, PDO_OCI and PSpell. Install them from PECL or switch to another library.
  • Session INI changes are deprecated for session.sid_length, session.sid_bits_per_character, session.use_only_cookies, session.use_trans_sid, session.trans_sid_tags, session.trans_sid_hosts and session.referer_check. Remove them from php.ini and .user.ini unless you need them.
  • mysqli: mysqli_ping(), mysqli_kill() and mysqli_refresh() are deprecated.
  • round() throws ValueError for an invalid mode. The old pre-rounding step was removed, so a few edge results change.
  • exit() behaves like a function. It now respects strict_types and throws TypeError for an invalid type.
  • JIT defaults changed. If you turned JIT on only by setting opcache.jit_buffer_size, you must now set opcache.jit as well.
  • bcrypt cost defaults to 12 instead of 10, so password hashing is slower. Existing hashes still verify.
  • Name collisions. Any function or class newly added in 8.4 breaks code that already defines it. Guard polyfills with function_exists().
  • Minimum libraries rose: libcurl 7.61.0, libxml2 2.9.4, OpenSSL 1.1.1. 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.1-8.4 \
  --ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .

testVersion 8.1-8.4 reports problems that affect any version in the range, so the code stays safe while 8.1 and 8.4 coexist. Switch to 8.4 after cutover. The ^10.0.0@dev constraint is the one the PHPCompatibility README shows on 22 Oct 2026. It needs PHP_CodeSniffer 4.0.2 or newer.

Rector can apply some fixes, such as explicit nullable types. Review every diff.

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

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

Leave plugin and theme code you do not own out of the scan and the rewrite. Update it instead. WordPress steps are in change the PHP version.

Last reviewed on 22 October 2026.