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.
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.
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.
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.
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
nulldefault (see the example above) trigger_error()withE_USER_ERROR, and theE_STRICTconstant- omitting
$escapeinfputcsv(),fgetcsv(),str_getcsv()and theSplFileObjectCSV methods mysqli_ping(),mysqli_kill(),mysqli_refresh()and theMYSQLI_REFRESH_*constants- changing
session.sid_length,session.sid_bits_per_character,session.use_only_cookiesorsession.use_trans_sid; theSIDconstant;session_set_save_handler()with more than two arguments lcg_value()(useRandom\Randomizer::getFloat())xml_set_object()and method names passed as plain strings to thexml_set_*_handler()functions- the ISO-string
DatePeriodconstructor (useDatePeriod::createFromISO8601String()) ReflectionMethod::__construct()with one argument (useReflectionMethod::createFromMethodName())stream_context_set_option()with two arguments, and unserialising strings with the uppercaseStag- the two-argument forms of
pg_fetch_result(),pg_field_prtlen()andpg_field_is_null() ldap_connect()with more than two arguments andldap_exop()with more than four- raising zero to a negative power with
**orpow(), 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_argvon the web. Filling$_SERVER['argc']and$_SERVER['argv']from the query string outside the CLI is deprecated; read$_GETor$_SERVER['QUERY_STRING']instead.- Output from inside an output handler. Echoing within a user output handler is deprecated.
__debugInfo()returningnull. Deprecated; return an empty array.intl.error_level. Deprecated; check errors yourself or enableintl.use_exceptions.DATE_RFC7231. This constant andDateTimeInterface::RFC7231are deprecated because they ignore the time zone.
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
phpVersionrange (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-rundiff with that in mind, or hold the 8.4 set until production has switched. php -mon the current server, filtered forimap,oci8,pdo_ociandpspell, 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
- Production on 8.2.35 before anything else, so the migration is never racing PHP2Shell.
- 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.
- Extensions rebuilt for 8.4, checked with
php -mand a webphpinfo()page, with PECL builds or code changes for anything unbundled. - Composer pointed at 8.4:
config.platform.phpof8.4.27on a branch,composer update, thencomposer check-platform-reqson the staging site, because the platform setting alone can let incompatible packages through. - Every notice recorded:
error_reporting = E_ALL, written to a log file and never to the page. The 8.4php.ini-productiontemplate excludesE_DEPRECATED, so staging must override it. - 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.
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.
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.