Upgrade guide: PHP 8.0 to 8.4
PHP 8.0 to 8.4: four releases of deprecations, and a few real breaks
If your code runs on PHP 8.0, the hardest break is already behind you. The route to 8.4 is mostly deprecations that build up from 8.1 to 8.4, plus a few removals that end in fatal errors. PHP 8.0 has been end of life since November 2023, so patch PHP2Shell on that server before you start.
Last reviewed against php.net's 8.1, 8.2, 8.3 and 8.4 migration guides and the PHP2Shell advisories.
Fix first, then migrate
An 8.0 server needs a backported build before the migration starts
There is no 8.0 release for PHP2Shell, and there will not be one: the branch closed in 2023. Moving to 8.4 is the lasting fix, but the code work needs proper testing first, and since 26 October 2026 both CVEs appear in CISA's Known Exploited Vulnerabilities catalogue.
- If a distribution, host or support vendor supplies your PHP, ask whether its 8.0 package now carries the fix.
- If you compile PHP, the security team's
PHP-8.0-CVE-2026-109618-109619branch holds a minimal source backport. It has no version number of its own. - If you can do neither quickly, CISA's guidance is that PHP systems on 8.1 or below should come offline.
Where 8.0 sits
| Where you are | PHP 8.0, end of life since 26 November 2023; last release 8.0.30 |
|---|---|
| Already behind you | The 7.4 to 8.0 break: removed functions, stricter errors, new comparison rules |
| Still ahead | 8.1, 8.2, 8.3 and 8.4: mostly deprecations, a handful of hard failures and four unbundled extensions |
| Where you land | PHP 8.4.27 or later, with security fixes until 31 December 2028 |
Confirm the exact build first: checking your PHP version. Other starting points are on the upgrade hub.
8.0 to 8.1
PHP 8.1: the release that starts the deprecation flood
Two 8.1 deprecations, null arguments and missing return types, can each touch many lines of a code base that is clean on 8.0. The database defaults are the change most likely to alter behaviour.
Null where PHP expects a scalar
What changed: Built-in functions no longer quietly accept null for string, integer, float or boolean parameters that are not marked nullable. Each such call now logs a deprecation.
What to look for: Calls like strlen($value), trim($input) or strtotime($date) where the argument comes from an optional field or a nullable column. Supply a default where the value enters, or cast it, rather than suppressing the notice.
Tentative return types on built-in classes
What changed: Overriding a method of a built-in class without a compatible return type logs a deprecation when the class loads. php.net describes this as a transition: once strict checking arrives, the mismatch becomes fatal and #[\ReturnTypeWillChange] stops helping.
What to look for: Subclasses of ArrayIterator, SplObjectStorage, PDOStatement or SessionHandler. Declare the return types now; the attribute only buys time.
Database extensions change their defaults
What changed: mysqli now raises mysqli_sql_exception on errors by default. Numeric columns now reach PHP as integers and floats, not strings, when PDO SQLite is used or PDO MySQL emulates prepared statements.
What to look for: Error handling built around false returns from mysqli_query(), and identity checks against string values read from the database.
Lossy float to integer conversion
What changed: Implicitly turning a float with a fractional part into an integer is deprecated. That covers array keys such as $a[15.5], int parameters in coercive mode and operators that work on integers.
What to look for: Array indexes and pagination offsets computed by division without rounding.
Reserved words and $GLOBALS
What changed: never becomes a reserved word that can no longer name a class, interface or trait, and readonly becomes a keyword (it may still name a function). Writing to $GLOBALS as a whole, rather than to one element, is now an error.
What to look for: Classes, interfaces or traits named Never, and old code that resets globals with array_pop($GLOBALS) or similar.
HTML escaping
What changed: The default flags of htmlspecialchars(), htmlentities() and their decoders are now ENT_QUOTES | ENT_SUBSTITUTE, so apostrophes are encoded and bad UTF-8 is substituted rather than wiping the string.
What to look for: Snapshot tests of rendered HTML, and code that read an empty return value as an encoding error.
php.net: migrating from 8.0 to 8.1
8.1 to 8.2
PHP 8.2: declare your properties, and watch string functions
Dynamic properties
What changed: Writing to a property a class has not declared is deprecated. A class can opt back in with #[\AllowDynamicProperties], which its child classes inherit; stdClass and classes with __get() and __set() are exempt.
What to look for: Models, DTOs and controllers that accept arbitrary fields. Declared properties are the long-term answer.
String and callable forms
What changed: "${var}" and "${expr}" interpolation are deprecated, as are relative callables such as "self::method" and ["parent", "method"].
What to look for: Templating helpers, event and hook registration that passes callbacks as strings.
Case-insensitive functions use ASCII rules
What changed: strtolower(), strtoupper(), lcfirst(), ucfirst(), ucwords(), stristr(), stripos(), strripos() and str_ireplace() no longer follow the locale. array_change_key_case() and SORT_FLAG_CASE sorting follow suit.
What to look for: Sites that set a locale with setlocale() to handle accented text; move those calls to the mb_* equivalents.
Sorting, splitting and exporting
What changed: ksort() and krsort() compare numeric string keys using the standard PHP 8 rules under SORT_REGULAR. str_split('') returns an empty array, and var_export() writes class names with a leading backslash.
What to look for: Tests that pin the order of sorted keys, and generated PHP files written with var_export() and compared as text.
mysqli without libmysql
What changed: mysqli can now be built only against mysqlnd. The mysqli.reconnect setting and the driver's reconnect property are gone, and MYSQLI_IS_MARIADB is deprecated.
What to look for: Long-running scripts that relied on automatic reconnection; they now need to reconnect themselves.
php.net: migrating from 8.1 to 8.2
8.2 to 8.3
PHP 8.3: exceptions in place of warnings
A stack limit that throws
What changed: Code that comes very close to overflowing the call stack may now throw an Error once it uses more than zend.max_allowed_stack_size minus zend.reserved_stack_size.
What to look for: Deep recursion in tree walkers, recursive template rendering and hand-written parsers.
The date extension throws its own exceptions
What changed: New DateError and DateException hierarchies replace warnings and generic exceptions. DateTime::modify() with a bad string now throws DateMalformedStringException; the procedural date_modify() is unchanged.
What to look for: Places that accept free-text dates and test the result against false.
Serialised data and number formatting
What changed: unserialize() raises warnings where it raised notices, plus a new warning when bytes are left over. number_format() with negative decimals now rounds to the left of the decimal point instead of ignoring the argument.
What to look for: Session and cache stores holding serialised strings, and financial reports that format to tens or hundreds.
Calls that are now deprecated
What changed: get_class() and get_parent_class() with no argument, ReflectionProperty::setValue() with a single argument, assert_options(), ldap_connect() with separate host and port, and a negative width in mb_strimwidth().
What to look for: For static properties, php.net's replacement is to pass null as the first argument to setValue().
range() validates its input
What changed: Arrays, objects and resources as limits throw TypeError; zero, negative-on-increasing, infinite or NAN steps throw ValueError.
What to look for: Calls whose limits come from request parameters.
php.net: migrating from 8.2 to 8.3
8.3 to 8.4
PHP 8.4: where the remaining hard failures are
8.4 carries most of this route's genuine breakage, through the unbundled extensions, new global names and stricter validation, as well as the implicit nullable deprecation.
Implicit nullable types, including the form 8.0 allowed
What changed: PHP 8.0 deprecated optional parameters placed before required ones but exempted Type $param = null. PHP 8.4 removes that exemption: a typed parameter that accepts null only because of its default is deprecated everywhere.
What to look for: Both shapes: function log(Logger $log = null), and before a required parameter, function get(Cache $cache = null, string $key), which php.net says should become ?Cache $cache with no default at all.
IMAP, OCI8, PDO_OCI and PSpell are unbundled
What changed: All four now live in PECL rather than in PHP itself, with releases published as demand requires.
What to look for: Mailbox readers using imap_*, Oracle access through oci_* or an oci: PDO DSN, and spell checking with pspell_*.
exit() and die() check their argument
What changed: Both now act like functions: they respect strict_types, apply normal type coercion and throw TypeError on invalid values.
What to look for: Exit calls passed arrays, objects or null.
Error level changes
What changed: E_STRICT is removed from the engine and its constant deprecated; trigger_error() with E_USER_ERROR is deprecated.
What to look for: Error-handler bitmasks that include E_STRICT, and code that raises fatal user errors; throw an exception instead.
New global functions and classes
What changed: array_find(), array_any(), array_all(), array_find_key(), mb_trim(), mb_ltrim(), mb_rtrim(), mb_ucfirst(), mb_lcfirst(), request_parse_body() and the classes Deprecated and RoundingMode join the global namespace.
What to look for: Unguarded helper definitions with the same names; they now fail with a redeclaration error.
Stricter argument validation
What changed: round() throws ValueError for an invalid mode instead of falling back to half-up, str_getcsv() rejects separators and enclosures that are not exactly one byte, and GD's image output functions reject invalid quality values.
What to look for: Wrapper functions that pass configuration values straight through to these calls.
Smaller removals
What changed: DOMImplementation::getFeature() is gone, the GMP class is final, PHP_DEBUG and PHP_ZTS are booleans, and the bundled PCRE2 10.44 reads {,3} as a quantifier.
What to look for: Classes extending GMP, and regular expressions that contain a literal {,.
php.net: 8.4 backward incompatible changes, 8.4 deprecated features, extensions removed in 8.4
Deprecations
Clear the old notices, and know what 8.5 adds
Code that has run on 8.0 for years may still carry the deprecations 8.0 itself introduced. They are easy to overlook because they predate the move, and one of them already throws on 8.1.
Still waiting from 8.0
- A default before a required parameter. Deprecated in 8.0; from 8.1 a named-argument call to such a function throws
ArgumentCountError. - Comparators that return booleans.
usort()callbacks written as$a > $bshould return an integer;$a <=> $bdoes it. - Reflection helpers.
ReflectionParameter::getClass(),isArray()andisCallable()give way togetType(). - The procedural Zip API.
zip_open()andzip_read()are deprecated in favour ofZipArchive.
Next in line: PHP 8.5
- Close functions for objects that free themselves.
curl_close(),curl_share_close(),imagedestroy()andfinfo_close()are deprecated, because their objects are released automatically. __sleep()and__wakeup(). Soft-deprecated in favour of__serialize()and__unserialize().- Driver constants on the PDO class. Names such as
PDO::MYSQL_ATTR_INIT_COMMANDare deprecated; usePdo\Mysql::ATTR_INIT_COMMANDand the other driver classes.
Tooling
Checking code that has to run on 8.0 and 8.4 at once
Fixes for this route are best shipped while production is still on 8.0, so the code needs to work on both ends of the range for a while. Open source analysers can check against that whole range.
- PHPStan with a version range. Since PHPStan 2.0,
phpVersionaccepts aminandmax, so one analysis covers every version from 8.0 to 8.4. - PHPCompatibility with a version range. A
testVersionof8.0-8.4makes the PHP_CodeSniffer standard report anything that breaks on any release in between. Coverage of PHP 8 changes sits in its 10.0 pre-release line (10.0.0-alpha2, November 2025), not in the 2019 stable release. - Rector, driven by composer.json. Called with no arguments,
withPhpSets()takes the PHP version fromcomposer.json, so its rewrites follow your declared minimum. Raise that minimum only when 8.0 support can go. - Composer.
composer prohibits php 8.4reports the packages that would stop the target from installing.
# phpstan.neon: check code that must run on both 8.0 and 8.4
parameters:
phpVersion:
min: 80000
max: 80400 # compatibility sniffs across the whole range you must support
vendor/bin/phpcs -p . --standard=PHPCompatibility --runtime-set testVersion 8.0-8.4
vendor/bin/phpstan analyse src
# packages whose constraints rule out 8.4
composer prohibits php 8.4 Documentation: PHPStan configuration, PHPCompatibility, PHP_CodeSniffer, Rector set lists, Composer commands.
Testing and rollout
Four things to have in place before 8.4 goes live
A staging copy on 8.4.27 or later
Same operating system family, web server and PHP handler as production. Anything older than 8.4.27 on the 8.4 branch is still exposed to PHP2Shell.
Extension parity
Compare php -m and a web phpinfo() page on both servers; the CLI and the web server can load different ini files. Plan PECL builds for any of the four extensions 8.4 unbundled.
Dependencies resolved for 8.4
Set config.platform.php to 8.4.27, update and commit the lock file, then run composer check-platform-reqs on staging to test the real runtime rather than the faked one.
Deprecations triaged, not muted
Log with error_reporting = E_ALL and display_errors = Off, group the entries by message, and fix the biggest sources first. A pattern repeated across many files can often be fixed in one reviewed change.
Then roll out
Walk through every page type, form, payment and email on
staging, then the scheduled jobs and command line tools, which
read the CLI's own php.ini. Switch production during
a quiet period with the 8.0 runtime still installed but idle,
keep the error log open for the first hours, and confirm the live
version afterwards.
Rollback, and the PHP2Shell catch
A rollback plan for this route returns you to 8.0, so it is only as safe as that build. Falling back to an 8.0 without the backport reopens a vulnerability with known exploitation. Write down the switch-back steps and any database changes to undo, set a trigger for using them, and treat a rollback as a short pause before rolling forward again.
Platform constraints
An 8.0 site may be on an older platform release too
Several platforms raised their minimum above 8.0 some time ago, so the application upgrade may be the bigger job and decides the order.
- Drupal: Drupal 10 needs at least PHP 8.1, so a site on 8.0 is on an older, unsupported Drupal. 10.4 to 10.6 and 11.1 to 11.4 all accept 8.4.
- Laravel: Laravel 10 already requires 8.1. Laravel 11, 12 and 13 support PHP 8.4.
- PrestaShop: the 8.x releases support PHP 7.2 to 8.1; PrestaShop 9.0 recommends 8.4.
- Moodle: Moodle 4.2 set PHP 8.0 as its minimum; PHP 8.4 is usable from Moodle 5.0.
- Magento 2 and OpenMage: Magento 2.4.8 lists 8.3 and 8.4; OpenMage has required 8.1 since 20.14.0.
- Joomla, Nextcloud and WooCommerce: Joomla 5 needs 8.1 and Joomla 6 recommends 8.4; Nextcloud 33 to 35 support 8.4; WooCommerce wants 8.3 or greater and is tested up to 8.4.
Installing 8.4 on the server: how to update PHP, with guides for Linux, cPanel, Plesk, Docker, PHP-FPM and Windows and IIS. Related routes: 7.4 to 8.4, 7.4 to 8.3, 8.1 to 8.4, 8.1 to 8.3, 8.2 to 8.4 and 8.2 to 8.3.
Quick answers
PHP 8.0 to 8.4 questions
My code already runs on PHP 8.0. How much will break on 8.4?
Far less than broke between 7.4 and 8.0. Most of what changes from 8.1 to 8.4 is deprecation, which logs a notice and carries on. The changes that do stop a request are narrower: the four extensions moved to PECL, new global names colliding with your own, stricter argument checks, and functions with optional parameters before required ones when called with named arguments.
Why is staying on PHP 8.0 riskier than staying on 8.1?
Neither gets a PHP2Shell release from the PHP project, but 8.0 has been out of support since 26 November 2023 and 8.1 only since 31 December 2025. That is more than two extra years without PHP project fixes for anything found in between. Both need a backported build now and a migration soon.
Do deprecation notices break my site?
No. A deprecation is logged and the code continues. They become a problem when error display is switched on in production, when the log fills so fast that real errors are lost, or when a later release removes the behaviour.
Is it acceptable to hide deprecation notices on the live server?
Hiding them from visitors is right: keep display_errors off in production. The php.ini-production file that ships with 8.4 already leaves E_DEPRECATED out of error_reporting. What matters is that staging logs them in full, so they get fixed rather than forgotten.
Do I need to upgrade my framework or CMS as well as PHP?
Very likely. Several platforms have since raised their minimum to 8.1 or higher: Drupal 10 needs 8.1, Laravel 10 supports 8.1 to 8.3, and PrestaShop 8.x stops at 8.1. Check your platform's requirements page before planning the PHP move.
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.