Upgrade guide: PHP 8.2 to 8.4

PHP 8.2 to 8.4: two releases, supported until the end of 2028

With PHP 8.2 ending on 31 December 2026, a move to 8.4 crosses two releases and lands on a branch that keeps receiving security fixes until 31 December 2028, a year beyond 8.3. Most of the code work comes from 8.4: nullable types, exit() and four extensions leaving the core. Patch to 8.2.35 before you start.

Last reviewed against php.net's 8.3, 8.4 and 8.5 migration guides and the PHP2Shell advisories.

Fix first, then migrate

8.2.35 now; 8.4 when it has passed its tests

The PHP project shipped 8.2.35 with the PHP2Shell fixes on 21 October 2026, so a supported 8.2 server needs only a point update to close them. Do that first and on its own. The 8.4 migration is a separate piece of work with its own testing, and mixing the two delays the security fix for no gain.

  • After updating, restart PHP-FPM or the web server so every worker runs the new build.
  • Check the running version from a web request; the command line PHP can differ.
  • Where PHP comes from a distribution, rely on its security notice rather than the version string.

Servers on 8.1 or older have no such release; their options are on the end of life page.

From 8.2: 8.3 or 8.4?

8.3 8.4
Releases of changes to absorb One (8.3) Two (8.3 and 8.4)
Security fixes until 31 December 2027 31 December 2028
First build with the PHP2Shell fix 8.3.36 8.4.27
IMAP, OCI8, PDO_OCI, PSpell Still bundled Moved to PECL

The shorter option has its own guide: PHP 8.2 to 8.3. Every route is on the upgrade hub.

Where the effort goes

Four 8.4 changes that set the size of the job

8.3 asks comparatively little of code that runs cleanly on 8.2. These four 8.4 changes account for most of what remains.

3D code windows, for implicit nullable types

Implicitly nullable parameters

A deprecation that can reach a large number of function signatures. Each fix is mechanical and runs on 8.2 as well, so it can ship before the switch.

3D servers going down and coming back, for changes to exit() and die()

exit() and die() as functions

An invalid argument now throws a TypeError. The calls at risk tend to sit in error paths that tests seldom reach.

3D ring of connected devices, for extensions moved out of PHP

Four extensions unbundled

IMAP, OCI8, PDO_OCI and PSpell are no longer part of PHP. If you use one, plan its replacement or a PECL build early.

3D clipboard under a magnifying glass, for new global names that can clash

New global names

Functions such as array_find() and classes such as RoundingMode now exist in PHP itself. Helpers that declare the same names without a guard stop with a redeclaration error.

First release: 8.3

PHP 8.3: the short half of this route

Exceptions from the date extension

What changed: Warnings and generic exceptions give way to DateError and DateException subclasses; DateTime::modify() throws DateMalformedStringException on bad input instead of returning false.

What to look for: Date handling that checks for false.

range() and number_format()

What changed: range() now throws for invalid limits and steps and returns characters for mixed digit and letter limits. number_format() applies negative decimals by rounding left of the point.

What to look for: Generated lists and financial rounding.

Class and array details

What changed: Trait static properties get per-class storage, appending after a negative key on an empty array continues from that key plus one, and interface constant visibility is checked.

What to look for: Trait-based shared state and arrays seeded with negative indexes.

Deprecations

What changed: get_class() or get_parent_class() with no argument, string ++ and -- on empty or non-numeric values, assert_options(), and single-argument ReflectionProperty::setValue().

What to look for: static::class, str_increment(), zend.assertions and setValue(null, $value) are the replacements.

Runtime limits and new names

What changed: A new stack-size limit can turn near-overflows into an Error, and 8.3 adds global functions such as json_validate() and mb_str_pad().

What to look for: Deep recursion, and unguarded helpers with those names.

Full detail: php.net, migrating from PHP 8.2 to 8.3.

Second release: 8.4

PHP 8.4 in full: failures, deprecations and quiet changes

The implicit nullable deprecation has a variant that is easy to miss, where the parameter sits in front of a required one:

<?php
// Before, deprecated in PHP 8.4: $rate is nullable only through its default
// function price(Product $product, Rate $rate = null, string $currency) {}

// After, php.net's form: explicit nullable, no default, as $currency is required
function price(Product $product, ?Rate $rate, string $currency) {}

Changes that can stop a request

exit() and die()

What changed: No longer special constructs that cast anything to a string: they accept a string or integer, respect strict_types and throw TypeError otherwise.

What to look for: die($response) where the value may be an array or object.

Unbundled extensions

What changed: IMAP, OCI8, PDO_OCI and PSpell now come from PECL, released as demand requires.

What to look for: imap_*, oci_*, oci: DSNs and pspell_* calls, and provisioning scripts that install them as standard PHP packages.

Redeclaration errors

What changed: New global functions (array_find(), array_find_key(), array_any(), array_all(), mb_trim(), mb_ucfirst(), fpow(), http_get_last_response_headers() and more) and global classes (Deprecated, RoundingMode, StreamBucket) can clash with your own.

What to look for: Unconditional function and class declarations in the global namespace.

New exceptions for bad input and recursion

What changed: Recursion found during a comparison throws an Error. round() with an unknown mode, str_getcsv() with a separator or enclosure longer than one byte, an invalid php_uname() mode and bad allowed_classes options to unserialize() all throw ValueError or TypeError.

What to look for: Wrappers that pass settings straight through, and comparisons of object graphs that refer back to themselves.

Classes you can no longer extend or clone

What changed: GMP is now final, and cloning a DOMXPath object throws an Error.

What to look for: Subclasses of GMP in maths libraries, and code that copies DOMXPath objects.

Deprecated in 8.4

Each of these still works but logs a notice. Most of the fixes also run on 8.2, so they can ship ahead of the switch. A few replacements only arrived in 8.3, namely DatePeriod::createFromISO8601String(), ReflectionMethod::createFromMethodName(), Random\Randomizer::getFloat() and stream_context_set_options(), so adopt those once 8.2 is gone.

  • typed parameters made nullable only by a null default (see the example above)
  • trigger_error() with E_USER_ERROR, and the E_STRICT constant
  • omitting $escape in fputcsv(), fgetcsv(), str_getcsv() and the SplFileObject CSV methods
  • mysqli_ping(), mysqli_kill(), mysqli_refresh() and the MYSQLI_REFRESH_* constants
  • changing session.sid_length, session.sid_bits_per_character, session.use_only_cookies or session.use_trans_sid; the SID constant; session_set_save_handler() with more than two arguments
  • lcg_value() (use Random\Randomizer::getFloat())
  • xml_set_object() and method names passed as plain strings to the xml_set_*_handler() functions
  • the ISO-string DatePeriod constructor (use DatePeriod::createFromISO8601String())
  • ReflectionMethod::__construct() with one argument (use ReflectionMethod::createFromMethodName())
  • stream_context_set_option() with two arguments, and unserialising strings with the uppercase S tag
  • the two-argument forms of pg_fetch_result(), pg_field_prtlen() and pg_field_is_null()
  • ldap_connect() with more than two arguments and ldap_exop() with more than four
  • raising zero to a negative power with ** or pow(), and naming a class _

Behaviour and configuration changes

Configuration

What changed: The JIT is now off by way of opcache.jit=disable, with opcache.jit_buffer_size defaulting to 64M. The E_STRICT level is gone, and the Apache module needs Apache 2.4.

What to look for: php.ini files that enabled the JIT through the buffer size alone, and error-reporting masks that still name E_STRICT.

Database drivers

What changed: Several PDO MySQL attributes, including PDO::ATTR_EMULATE_PREPARES, are now booleans; the PDO_PGSQL driver gives credentials in the DSN priority over constructor arguments; MySQL server wait timeouts report error code 4031 rather than 2006 on MySQL 8.0.24 and later.

What to look for: Reconnect logic keyed on error 2006, and strict comparisons of PDO attribute values.

Strings, XML and SOAP

What changed: SimpleXML no longer rewinds an iterator as a side effect of methods such as asXML(); strcspn() with an empty character list returns the string length; http_build_query() handles backed enums; SoapClient::$typemap is an array rather than a resource.

What to look for: Loops over SimpleXML elements that call asXML() inside them, and is_resource() checks on SOAP client properties.

Files and constants

What changed: Uploaded and tempnam() file names are 13 bytes longer, and PHP_DEBUG and PHP_ZTS are booleans rather than integers.

What to look for: Database columns or path checks sized to the old temporary file name length.

php.net: incompatible changes, deprecated features, removed extensions, new functions, new classes.

Before the next step

Leave 8.4 ready for 8.5

8.4 should hold until the end of 2028, but the next move will come. Beyond emptying the 8.3 and 8.4 deprecation log, these 8.5 changes are worth knowing about now:

  • register_argc_argv on the web. Filling $_SERVER['argc'] and $_SERVER['argv'] from the query string outside the CLI is deprecated; read $_GET or $_SERVER['QUERY_STRING'] instead.
  • Output from inside an output handler. Echoing within a user output handler is deprecated.
  • __debugInfo() returning null. Deprecated; return an empty array.
  • intl.error_level. Deprecated; check errors yourself or enable intl.use_exceptions.
  • DATE_RFC7231. This constant and DateTimeInterface::RFC7231 are deprecated because they ignore the time zone.

php.net: PHP 8.5 deprecations

Tooling

One pass of open source checks across 8.2 to 8.4

While production stays on 8.2, every fix has to run there too, so check against the full range rather than 8.4 alone. These tools analyse source code; they do not run it.

  • PHPStan accepts a phpVersion range (PHPStan 2.0 onwards), as in the configuration below.
  • PHPCompatibility, a standard for PHP_CodeSniffer, takes --runtime-set testVersion 8.2-8.4. Its coverage of recent PHP versions is in the 10.0 pre-releases (10.0.0-alpha2 is the latest, from November 2025), which its README installs with ^10.0.0@dev.
  • Rector can apply its PHP sets up to 8.4 with withPhpSets(php84: true), but some of those rules rewrite code to use features 8.2 lacks. Review the --dry-run diff with that in mind, or hold the 8.4 set until production has switched.
  • php -m on the current server, filtered for imap, oci8, pdo_oci and pspell, tells you at once whether the unbundling affects you.
# phpstan.neon: keep the code valid on 8.2, 8.3 and 8.4 during the move
parameters:
    phpVersion:
        min: 80200
        max: 80400

Project documentation: PHPStan, PHPCompatibility, PHP_CodeSniffer, Rector.

Testing and rollout

A test and switch plan that respects the 8.2 deadline

  1. Production on 8.2.35 before anything else, so the migration is never racing PHP2Shell.
  2. 8.4.27 or later installed alongside, as a separate PHP-FPM pool or a per-site setting in your control panel, serving a staging copy.
  3. Extensions rebuilt for 8.4, checked with php -m and a web phpinfo() page, with PECL builds or code changes for anything unbundled.
  4. Composer pointed at 8.4: config.platform.php of 8.4.27 on a branch, composer update, then composer check-platform-reqs on the staging site, because the platform setting alone can let incompatible packages through.
  5. Every notice recorded: error_reporting = E_ALL, written to a log file and never to the page. The 8.4 php.ini-production template excludes E_DEPRECATED, so staging must override it.
  6. Real use of the site, including scheduled tasks and command line scripts, and then the switch in a quiet period.

Rollback: 8.2.35 this year, 8.3 after it

Until 31 December 2026 the obvious fallback is the 8.2.35 pool you kept running. Never fall back to an 8.2 build below 8.2.35, which reopens PHP2Shell. From January 2027, 8.2 is out of support, so a better safety net is 8.3.36 or later: keep the code compatible with 8.3 while you test, and you have a supported runtime to retreat to if 8.4 causes trouble. Write the switch-back steps down before the cutover, including any database changes to reverse.

Platform constraints

Platforms that favour 8.4 for sites leaving 8.2

Several platforms already treat 8.2 as on its way out, and some point to 8.4 as their preferred version.

  • Nextcloud: 33 and 34 mark 8.2 deprecated and 33 recommends 8.4; 35 drops 8.2 altogether.
  • Magento 2: 2.4.7 covers 8.2 and 8.3; for 8.4 you need 2.4.8.
  • Moodle: 5.0 and 5.1 have 8.2 as their minimum and can use 8.4.
  • Laravel: 11, 12 and 13 all support 8.4; 13 requires at least 8.3.
  • Drupal: 10.4 to 10.6 accept 8.2 to 8.4; 11.1 to 11.4 accept 8.3 and 8.4.
  • PrestaShop 9.0 and Joomla 6 both name 8.4 as their recommended version.
  • WooCommerce: needs at least 8.3, and its testing goes up to 8.4.
  • OpenMage: supports 8.4, noting that some warnings may be logged.

Server side: PHP update guides for Linux, cPanel, Plesk, PHP-FPM, Docker, Apache and Windows and IIS. Neighbouring routes: 8.2 to 8.3, 7.4 to 8.4, 8.1 to 8.4, 8.0 to 8.4, 7.4 to 8.3 and 8.1 to 8.3.

Quick answers

PHP 8.2 to 8.4 questions

What if the 8.4 move is not finished by 31 December 2026?

8.2.35 keeps PHP2Shell closed, but from 1 January 2027 the PHP project issues nothing further for 8.2. If 8.4 slips, a move to 8.3 first keeps you on a supported branch, and code written to run on 8.2 and 8.4 will generally run on 8.3 too, though it still needs a test there. Some suppliers also patch 8.2 packages beyond php.net's date; check their stated policy.

The PHP 8.2 to 8.3 guide

From 8.2, is 8.4 much more work than 8.3?

It is 8.3's changes plus 8.4's. For many applications 8.4 adds a batch of deprecations to clear and a few hard failures to test for. If you depend on IMAP, OCI8, PDO_OCI or PSpell, it adds a real piece of work, because those extensions no longer ship with PHP.

Does PHP 8.4 still include the IMAP extension?

No. IMAP was moved out of the PHP distribution to PECL in 8.4, along with OCI8, PDO_OCI and PSpell. You can build it from PECL where your platform allows, but php.net says new PECL releases will appear only as demand requires, so a replacement in code is the more durable route.

Do I need to change php.ini for PHP 8.4?

Check four things. Remove E_STRICT from any error_reporting mask, because the level no longer exists. If you used the JIT, set opcache.jit explicitly. Stop changing the deprecated session settings such as session.sid_length. And drop opcache.consistency_checks if it is still there, since 8.3 removed it.

Can the same code run on 8.2 and 8.4 during the switch?

Yes, if you avoid features newer than 8.2 until the switch is done, such as typed class constants from 8.3 or property hooks and asymmetric visibility from 8.4. The fixes for the main 8.4 deprecations, such as explicit nullable types and passing the CSV escape argument, work on 8.2 as well; a few replacement functions, such as Random\Randomizer::getFloat(), only arrived in 8.3.

3D rocket illustration above the call to find every exposed PHP server

Know your exposure

Find every PHP server you run

Patching the server you know about is the easy part. A vulnerability scan finds the forgotten staging site, the old microsite and the appliance with PHP inside, then tells you which ones are still exposed.