Upgrade guide: PHP 7.4 to 8.3

Upgrading PHP 7.4 to 8.3: what breaks, what to search for and how to test

Moving from PHP 7.4 to 8.3 crosses four releases, and the largest set of breaking changes arrives with PHP 8.0. Close PHP2Shell on your current server first, then work through the 8.0 removals, the 8.1 to 8.3 deprecations and a staged test, so the new runtime goes live on 8.3.36 or later.

Last reviewed against php.net's migration guides for 8.0 to 8.3 and the PHP2Shell advisories.

Fix first, then migrate

Close PHP2Shell on the 7.4 server before touching the code

A 7.4 to 8.3 migration takes testing time, and a 7.4 server is exposed for every day of it. The PHP project made no 7.4 release for PHP2Shell: its fixes of 21 October 2026 cover 8.2 to 8.5 only. So treat the security fix and the migration as two separate jobs, and do the first one now.

  • If PHP came from a distribution, host or vendor, ask whether its 7.4 package carries the PHP2Shell backport. php.net says some suppliers ship backported fixes for branches it no longer supports.
  • If you build PHP yourself, the PHP security team's source-only branch PHP-7.4-CVE-2026-109618-109619 adds the fix without a new version number.
  • If neither is possible soon, CISA lists two triggers for taking a PHP system offline: running PHP 8.1 or earlier, or being unable to update straight away.

This route at a glance

Starting branch PHP 7.4: end of life since 28 November 2022, final release 7.4.33, no PHP2Shell release from the PHP project
Target branch PHP 8.3: security fixes only, until 31 December 2027
Lowest safe target build 8.3.36, the first 8.3 release carrying the PHP2Shell fixes
Releases crossed 8.0, 8.1, 8.2 and 8.3
php.net guides to read, in order 7.4 to 8.0, 8.0 to 8.1, 8.1 to 8.2, 8.2 to 8.3

Not sure which build you run? Start with how to check your PHP version. All the guides for other starting points are on the PHP upgrade hub.

Four releases in one move

Where the work sits between 7.4 and 8.3

You install one new runtime, but your code has to absorb the changes of four releases. They are far from equal: a site that runs cleanly on 8.0 usually has most of the hard work behind it.

3D laptop with a warning sign and alarm light, for the PHP 8.0 break

PHP 8.0

The breaking release

Removed functions, syntax that no longer compiles, warnings turned into exceptions and new rules for comparing numbers with text. Budget most of the effort here.

The PHP 8.0 changes
3D clipboard under a magnifying glass, for stricter built-in functions in 8.1

PHP 8.1

Stricter built-ins

Null passed to PHP's own functions starts raising notices, overridden internal methods need return types, and mysqli begins throwing exceptions.

The PHP 8.1 changes
3D tangled cable, for undeclared properties in 8.2

PHP 8.2

Undeclared properties

Setting properties a class never declared becomes deprecated, along with an old string interpolation style and two encoding helpers.

The PHP 8.2 changes
3D shield with a green tick above a device, for the short final step to 8.3

PHP 8.3

A short final step

Tighter rules in range(), exceptions from the date extension and a handful of deprecated calls. Little here should stop a site that survived 8.0.

The PHP 8.3 changes

Step 1 of 4

PHP 8.0: what changed and what to look for in 7.4 code

PHP 8.0 removed most of what 7.x had marked as deprecated and made the engine far less forgiving. Several of these changes stop a page dead; one of them changes results without any error at all.

<?php
var_dump(0 == "foo");  // 7.4: bool(true)   8.0 and later: bool(false)
var_dump(0 == "");     // 7.4: bool(true)   8.0 and later: bool(false)
var_dump(42 == "42");  // bool(true) on both

Comparing numbers with text

What changed: When == compares a number with a string that is not numeric, PHP 8.0 converts the number to a string and compares the two strings. 0 == "foo" and 0 == "", both true on 7.4, are false from 8.0. Numeric strings such as "42" still compare as numbers.

What to look for: Loose checks against zero or an empty string, switch statements fed with form or database values (a switch compares loosely), and in_array() or array_search() without the strict third argument. The code keeps running and quietly returns a different answer, so only tests reveal it.

Functions that no longer exist

What changed: PHP 8.0 deletes functions that 7.x had already deprecated, among them each(), create_function(), money_format(), get_magic_quotes_gpc(), restore_include_path(), hebrevc(), convert_cyr_string() and fgetss(). An __autoload() function is no longer called either.

What to look for: Search for each name across your own code, bundled libraries and old plugins. The replacements are a foreach loop for each(), an anonymous function for create_function(), NumberFormatter::formatCurrency() for money_format() and spl_autoload_register() for __autoload().

Syntax that stops compiling

What changed: Reading an offset with braces, as in $name{0}, is gone, as are the (real) and (unset) casts. Nested ternary expressions now need brackets, and a line beginning #[ is read as an attribute instead of a comment.

What to look for: These fail when the file is compiled, before any request is served, so lint every file with the 8.3 binary, templates and vendor code included. Swap braces for square brackets: $name[0].

Warnings promoted to exceptions

What changed: Situations that 7.4 reported as a warning now throw an Error or TypeError and end the request: writing a property on null, using an undefined constant written as a bare word, giving a built-in function the wrong number of arguments, calling count() on something that cannot be counted, and arithmetic on arrays or objects. Reading an undefined variable or array key moves up from a notice to a warning.

What to look for: Array keys written without quotes, such as $row[title], which 7.4 treated as the string 'title'. count() on values that can be null. Objects created implicitly by assigning to a property of an unset variable.

Constructors named after the class

What changed: A method with the same name as its class is no longer treated as the constructor; only __construct() is.

What to look for: Classes in old libraries or plugins that have no __construct() but do have a method named after the class. On 8.0 that method never runs when the object is created, so whatever it used to set up stays empty.

Handles that became objects

What changed: curl_init(), GD's image functions, openssl_pkey_new(), xml_parser_create(), socket_create() and others now return objects rather than resources.

What to look for: Success checks written as is_resource($handle). They now report failure even when the call worked; php.net's advice is to compare the return value with false instead.

PDO throws by default

What changed: PDO's default error mode moved from silent to exceptions, so a failed statement raises a PDOException.

What to look for: Connections created without setting PDO::ATTR_ERRMODE, followed by code that tests for false or reads errorInfo() after each query. Without a try block, one failed query now ends the page.

Error handlers and the @ operator

What changed: @ no longer hides fatal errors, and the default error_reporting level is now E_ALL.

What to look for: Handlers set with set_error_handler() that test error_reporting() == 0 to detect suppressed errors. php.net replaces that test with a bitmask check: !(error_reporting() & $errno).

Server-level changes

What changed: The XML-RPC extension left the PHP distribution for PECL, and under Apache the PHP module is now called php_module instead of php7_module.

What to look for: Calls to the xmlrpc_* functions, and Apache configuration or .htaccess blocks wrapped in a test for php7_module, whose settings silently stop applying.

A typical rewrite for one of the removed functions, each():

<?php
// PHP 7.4 and older; a fatal "undefined function" error from PHP 8.0
while (list($key, $value) = each($settings)) {
    echo "$key=$value\n";
}

// Works on 7.4 and on every PHP 8 release
foreach ($settings as $key => $value) {
    echo "$key=$value\n";
}

Full list: php.net, PHP 8.0 backward incompatible changes.

Step 2 of 4

PHP 8.1: stricter about how you call PHP's own functions

Most 8.1 changes are deprecations, which log a notice and carry on. The exceptions are the database extension defaults and the $GLOBALS restriction, which can stop code that used to work.

Null passed to built-in functions

What changed: Giving null to a parameter of one of PHP's own functions that does not accept it, such as the string argument of strlen(), trim() or htmlspecialchars(), now raises a deprecation notice. On 7.4 it was accepted without comment.

What to look for: Missing request fields, NULL database columns and unset configuration values flowing into string and array functions. On older code bases the notices can be very numerous, so collect them in staging logs rather than on screen.

Return types on overridden internal methods

What changed: A class that overrides a method of one of PHP's built-in classes must now declare a compatible return type, or a deprecation notice appears when the class loads. The #[\ReturnTypeWillChange] attribute silences it.

What to look for: Subclasses of ArrayObject, ArrayIterator, DateTime, PDO and similar. Treat the attribute as temporary: php.net says a future version will make such mismatches fatal.

mysqli throws exceptions

What changed: From 8.1 the mysqli reporting mode defaults to MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, so a failed connection or query throws mysqli_sql_exception instead of returning false.

What to look for: if (!$result) tests after mysqli_query() and connection code that inspects mysqli_connect_error(). While you add proper handling, mysqli_report(MYSQLI_REPORT_OFF) restores the old behaviour.

HTML escaping defaults

What changed: htmlspecialchars(), htmlentities() and their decode counterparts now default to ENT_QUOTES | ENT_SUBSTITUTE. A single quote becomes &#039;, and malformed UTF-8 is replaced with a substitution character instead of yielding an empty string.

What to look for: Tests that compare rendered HTML byte for byte, and code that treated an empty result as a sign of invalid input.

Deprecated helpers

What changed: FILTER_SANITIZE_STRING, strftime(), date_sunrise(), date_sunset() and the mhash() family are deprecated, as is implementing Serializable without __serialize() and __unserialize(). Appending to a variable that holds false ($list = false; $list[] = 1;) is deprecated too.

What to look for: Home-grown input filters built on FILTER_SANITIZE_STRING, and date output through strftime(), which php.net suggests replacing with date() or IntlDateFormatter::format().

Writes to the whole $GLOBALS array

What changed: Reading $GLOBALS and writing single elements still work, but writing to the array as a whole, for instance array_pop($GLOBALS), is now an error.

What to look for: Old bootstrap code that resets or merges into $GLOBALS wholesale.

Sources: 8.1 incompatible changes and 8.1 deprecations on php.net.

Step 3 of 4

PHP 8.2: properties must be declared

Code written for 7.4 may set properties wherever it needs them. From 8.2 that still works but logs a deprecation, which can fill a staging log quickly.

Undeclared (dynamic) properties

What changed: Creating a property the class never declared now raises a deprecation notice, unless the class has the #[\AllowDynamicProperties] attribute. stdClass and classes that handle access through __get() and __set() are unaffected.

What to look for: Objects that collect fields on the fly, such as models filled straight from database rows. Declaring the property is php.net's preferred fix; a WeakMap suits extra data hung on objects you do not own.

${} inside strings

What changed: The "${var}" and "${expr}" interpolation styles are deprecated.

What to look for: Templates, email bodies and hand-built SQL. Rewrite them as "$var" or "{$var}".

utf8_encode() and utf8_decode()

What changed: Both are deprecated. They only ever converted between ISO-8859-1 and UTF-8.

What to look for: Import and export code. The manual offers mb_convert_encoding(), UConverter::transcode() and iconv() as alternatives, each of which also handles other encodings such as Windows-1252.

Case conversion ignores the locale

What changed: strtolower(), strtoupper(), ucfirst(), ucwords(), stripos(), str_ireplace() and related functions now change ASCII letters only, as if the locale were "C".

What to look for: Code that calls setlocale() and expects accented letters to change case. Use the mb_* functions for that.

Smaller changes

What changed: Callables written as "self::method", "parent::method" or ["static", "method"] are deprecated, and str_split("") now returns an empty array.

What to look for: Callbacks passed as strings to call_user_func() or array_map() inside classes.

Sources: 8.2 incompatible changes and 8.2 deprecations on php.net.

Step 4 of 4

PHP 8.3: the last few adjustments

range() is stricter

What changed: Arrays, objects or resources as boundaries throw TypeError. A step of 0, a negative step on an increasing range, or an infinite or NAN argument throws ValueError, and several odd inputs now warn.

What to look for: range() calls whose boundaries come from user input or configuration.

Date errors become exceptions

What changed: The date extension now has its own error and exception classes. DateTime::modify(), for example, throws DateMalformedStringException for an invalid string where it used to return false with a warning.

What to look for: Date parsing of user input that checks for a false return. The new classes extend Exception, so a broad catch still works; a bare false test does not.

get_class() with no argument

What changed: Calling get_class() or get_parent_class() without an argument is deprecated.

What to look for: Use self::class, static::class or get_class($this), depending on which class name the code needs.

Incrementing strings

What changed: ++ on an empty string or a string that is not alphanumeric is deprecated, and so is -- on an empty or non-numeric string.

What to look for: Generated references and column labels built by incrementing strings. str_increment(), new in 8.3, is the supported way to step "a" to "b".

Other behaviour to test

What changed: unserialize() notices are now warnings, with a new warning for trailing bytes. number_format() rounds before the decimal point when given negative decimals. assert_options() is deprecated. On Windows, PHP 8.3 needs Windows 8 or Windows Server 2012 at least.

What to look for: Caches and sessions holding serialised data, and any reports that format numbers with negative precision.

Sources: 8.3 incompatible changes and 8.3 deprecations on php.net.

Deprecations

Notices to clear now, so the next upgrade is smaller

A deprecation is PHP telling you a behaviour may be withdrawn in a later release. PHP 8.3 is supported only until the end of 2027, so whatever you leave in place now is work waiting for the next move.

Left over from PHP 8.0

  • Optional parameters before required ones. Deprecated in 8.0; since 8.1 a call using named arguments throws ArgumentCountError. Drop the pointless default.
  • Sort callbacks returning true or false. Return an integer instead, for example with the spaceship operator: usort($rows, fn($a, $b) => $a <=> $b);
  • libxml_disable_entity_loader(). Deprecated because libxml 2.9 already disables external entities by default.
  • ReflectionParameter::getClass(). Use getType() and the ReflectionType classes.

What PHP 8.4 will ask for next

  • Implicitly nullable parameters. function save(Order $order = null) is deprecated in 8.4; write ?Order $order = null, which 7.4 already accepts.
  • Four extensions leave the core. IMAP, OCI8, PDO_OCI and PSpell move to PECL in 8.4.
  • E_STRICT. The level is removed and the constant deprecated in 8.4, so error handling that mentions it needs tidying.

php.net: migrating to PHP 8.4

Tooling

Open source tools that speed up the search

These tools read code without running it. That makes them quick at finding removed functions and syntax across thousands of files, and blind to behaviour that depends on runtime values, such as a loose comparison between a number and a string. Use them before testing, not instead of it.

  • php -l. PHP's own syntax check. Run with the 8.3 binary, it catches brace offsets, unbracketed nested ternaries and removed casts file by file.
  • PHP_CodeSniffer with the PHPCompatibility standard. PHPCompatibility is a set of sniffs for PHP_CodeSniffer that flags features removed, deprecated or introduced between PHP versions once you give it a testVersion. Note its release status: the last stable version, 9.3.5, dates from December 2019, and the 10.0 line that covers recent releases is still published as pre-releases (10.0.0-alpha2 in November 2025), installed per its README with ^10.0.0@dev.
  • Rector. Rector rewrites code using rule sets. ->withPhpSets(php83: true) in rector.php switches on every PHP set up to 8.3, and --dry-run shows the proposed changes as a diff without writing them. Review the diff like any other change.
  • PHPStan. PHPStan analyses code as if for the PHP version in its phpVersion setting (80300 for 8.3). Without that setting it reads config.platform.php from composer.json, and failing that uses the PHP running it.
  • Composer. composer prohibits php 8.3 (alias why-not) lists the installed packages whose requirements rule 8.3 out.
# Syntax check every file with the target binary
find . -name '*.php' -print0 | xargs -0 -n1 php -l

# Compatibility sniffs for PHP 8.3
vendor/bin/phpcs -p . --standard=PHPCompatibility --runtime-set testVersion 8.3

# Preview Rector's rewrites without changing files
vendor/bin/rector process --dry-run

# Which installed packages block PHP 8.3?
composer prohibits php 8.3

Run the lint with the 8.3 binary: put it first on the PATH or call it by its full path, because the php command on a 7.4 server still means 7.4.

Testing and rollout

A test and rollout plan for the 7.4 to 8.3 move

  1. Step 1

    Build a staging copy on 8.3.36 or later

    Use the same web server, PHP handler and operating system family as production, with a recent copy of the code and database. A target build older than 8.3.36 would put PHP2Shell straight back.

  2. Step 2

    Match the extensions

    Compare php -m on the 7.4 server with the new one. The web server and the command line can load different php.ini files, so also compare phpinfo() output from a web request and run php --ini to see which files the CLI reads. XML-RPC, if you used it, now comes from PECL.

  3. Step 3

    Let Composer resolve for 8.3

    On a branch, set config.platform.php to the target version, run composer update and commit the lock file. Composer's documentation warns that a faked platform can install packages that then fail at runtime, so run composer check-platform-reqs on the staging server, which checks the real PHP and extensions.

  4. Step 4

    Log at full error reporting

    The php.ini-production file that ships with PHP leaves deprecation notices out of error_reporting. On staging, report everything to a log and nothing to the screen, then read the log after every test run.

  5. Step 5

    Exercise the whole site, not just the home page

    Logins, forms, uploads, payments, outgoing email, search, imports and exports, plus cron jobs and command line scripts, which run under the CLI's own configuration.

  6. Step 6

    Cut over with the old runtime still installed

    Switch production to 8.3 in a quiet period, keep the previous PHP installed but idle, watch the error log closely for the first hours, then confirm the live version.

Composer platform setting for the branch

{
    "config": {
        "platform": {
            "php": "8.3.36"
        }
    }
}

Error logging on staging

; staging php.ini: show every notice in the log, none on screen
error_reporting = E_ALL
display_errors = Off
log_errors = On
error_log = /var/log/php/upgrade-83.log

The rollback plan, and its catch

Write down how to switch back before you switch over: which PHP handler or pool to re-enable, which caches to clear, and how to reverse any database changes made during the move. Then check what you would be rolling back to. Returning to a 7.4 build without the PHP2Shell backport reopens both CVEs on a server the internet can reach, and CISA added both to its Known Exploited Vulnerabilities catalogue on 26 October 2026. Keep any fallback window short and only onto a patched build.

Platform constraints

Your application may need its own upgrade first

Most of the platforms below need PHP 8.1 or later in their current releases, so software still on 7.4 is likely to be a major release behind. Check what yours accepts before you plan the PHP change, because it may set both the target and the order of work.

  • WordPress and WooCommerce: WordPress 7.0 and 7.1 run on PHP 7.4 to 8.5, so WordPress itself does not block the move; plugins and themes might. WooCommerce asks for PHP 8.3 or greater.
  • Drupal: drupal.org says no supported Drupal release runs on any PHP 7 version. Drupal 10.4 to 10.6 and Drupal 11.1 to 11.4 accept PHP 8.3.
  • PrestaShop: 8.0 to 8.2 stop at PHP 8.1, so a shop on 8.x needs PrestaShop 9 to run on 8.3.
  • Magento 2: Adobe lists PHP 8.3 for 2.4.7 (with 8.2) and 2.4.8 (with 8.4).
  • Magento 1 and OpenMage: OpenMage has required PHP 8.1 or later since 20.14.0, so on a 7.4 store the OpenMage upgrade and the PHP move have to happen together.
  • Joomla: Joomla 5 needs PHP 8.1 and recommends 8.3; Joomla 6 needs 8.3.
  • Laravel: Laravel 10 supports PHP 8.1 to 8.3, 11 supports 8.2 to 8.4, 12 supports 8.2 to 8.5, and 13 requires 8.3.
  • Moodle and Nextcloud: Moodle can use PHP 8.3 from 4.4 onwards; Nextcloud 35 supports 8.3, 8.4 and 8.5.

Installing 8.3 on the server

Once the code is ready, the server side is covered by the PHP update guides: Linux, cPanel, Plesk, shared hosting, Docker, PHP-FPM, Apache and Windows and IIS. Moving off 7.4 by another route? See 7.4 to 8.4, or the guides from 8.0, 8.1 to 8.3, 8.1 to 8.4, 8.2 to 8.3 and 8.2 to 8.4.

Quick answers

PHP 7.4 to 8.3 questions

Can I go straight from PHP 7.4 to 8.3, or should I stop at 8.0 first?

Straight to 8.3 is normal. PHP 8.0, 8.1 and 8.2 are either end of life or close to it, so there is no point running them in production on the way. What you should not skip is reading: work through php.net's migration guides for 8.0, 8.1, 8.2 and 8.3 in that order, because each assumes the previous one is done.

Will PHP 7.4 code run on 8.3 without changes?

Small, well-maintained code bases sometimes do. Anything that uses a function removed in 8.0, brace offsets, unbracketed nested ternaries or bare-word array keys will not, and loose comparisons may give different answers without any error. A lint run and a staging test on 8.3 tell you which case you are in.

Should I target PHP 8.4 instead of 8.3?

If your application and its extensions support 8.4, it buys an extra year: security fixes until 31 December 2028 against 31 December 2027 for 8.3. The extra work is 8.4's own changes, chiefly implicit nullable parameter types and four extensions leaving the core. Stop at 8.3 when your software tops out there.

The PHP 7.4 to 8.4 guide

Does moving to PHP 8.3 fix PHP2Shell?

Only if you land on 8.3.36 or a later 8.3 release, or on a supplier's 8.3 package that carries the fix. Earlier 8.3 builds are vulnerable. Check the exact version after the switch.

How to check your PHP version

What if the site breaks after switching to 8.3?

Switch back to the previous runtime, but only if that runtime is a 7.4 build carrying the PHP2Shell backport; an unpatched 7.4 reopens both vulnerabilities. Then read the PHP error log from the failed attempt, fix what it shows on staging and try again. If no patched 7.4 is available, weigh the risk of staying online on it before you fall back.

Should I take my website offline?

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.