Read this first
PHP 7.4 reached end of life on 28 Nov 2022. The PHP2Shell fixes for it exist only as a source backport branch, and no 7.4 release contains them. The way out is a move to a supported branch. The target is 8.4 (fixed in 8.4.27). See which version to pick and PHP 7.4.
You cannot skip the work between 7.4 and 8.4, but you can do it in one pass. This page lists what each version changed. Every item comes from the migration guides in the PHP manual, linked in the sources.
The order that works
- Back up files and the database. Test a restore.
- Copy the site to a staging environment that runs 8.4 and has the same extensions.
- Update the framework, CMS, plugins and Composer packages to versions that declare 8.4 support. Run
composer outdatedandcomposer why-not php 8.4to see what blocks you. - Scan your own code with PHPCompatibility (commands below).
- Fix what the scan finds, starting with the 8.0 items, which break code.
- Run staging with
error_reporting = E_ALLandlog_errors = On. Exercise every flow, then read the log. - Fix deprecations from 8.1 to 8.4. They do not break code today, but they fill logs and the features may be removed in a later version.
- Switch production, restart every PHP worker and confirm the version.
PHP 8.0: the changes that break code
8.0 is a major version. Most real breakage sits here.
- String and number comparison.
0 == "foo"was true and is now false. So are0 == ""and42 == "42foo". Search for loose==checks against user input or database values, and use===. - Warnings became exceptions. Arithmetic on a non-numeric string throws
TypeError.count()on a non-countable value throwsTypeError. An undefined constant throws anError(7.4 read it as a string). A wrong argument count to a built-in function throwsArgumentCountError. - Notices became warnings. Reading an undefined variable, array key or property now logs a warning. Old themes and plugins will fill the log with them.
- Removed functions.
each(),create_function(),__autoload(),money_format(),get_magic_quotes_gpc()andfgetss()are gone. Useforeach, anonymous functions andspl_autoload_register(). - Removed syntax.
$str{0}(use$str[0]), the(real)and(unset)casts, and methods named after their class as constructors (use__construct()). - Concatenation precedence.
"Sum: " . $a + $bnow adds first. Add parentheses where you meant the old order. - Named arguments.
call_user_func_array()now treats string keys as parameter names instead of ignoring them. Check calls that pass associative arrays. - Resources are objects.
curl_init()returns aCurlHandle, and the GD and OpenSSL functions return objects too. Code that checksis_resource($ch)now fails. Check forfalseinstead. @no longer hides fatal errors. Error handlers that testerror_reporting() == 0should testerror_reporting() & $errno.- Defaults.
error_reportingdefaults toE_ALL. PDO's default error mode is exceptions. - Reserved names.
matchis a keyword andmixedcannot be a class name.#[now starts an attribute instead of a comment. - Extensions and modules.
mbstring.func_overloadis gone, and XML-RPC moved to PECL. The Apache module name changed fromphp7_moduletophp_module, so oldLoadModuleandIfModulelines stop matching. - Deprecated in 8.0. A required parameter after an optional one,
libxml_disable_entity_loader(), and sort callbacks that return a boolean instead of an integer. Use<=>inusort().
PHP 8.1: mostly deprecations, two behavior changes
nullto built-in functions. Passingnullto a non-nullable parameter, such asstr_contains($x, null)ortrim($x), logs a deprecation. This is the biggest source of log noise on old WordPress code. Cast with(string) $xor use$x ?? ''.- mysqli throws. The default error mode changed from silent to exceptions. Wrap calls that relied on silent failure, or restore the old mode with
mysqli_report(MYSQLI_REPORT_OFF);. htmlspecialchars()defaults changed toENT_QUOTES | ENT_SUBSTITUTE, so'becomes'.- Return types of internal methods. Classes that implement
ArrayAccess,Iterator,CountableorJsonSerializableneed a compatible return type, or the#[\ReturnTypeWillChange]attribute, to avoid a deprecation. Serializablealone is deprecated. Add__serialize()and__unserialize().- Other deprecations.
strftime(),FILTER_SANITIZE_STRING, appending tofalseas if it were an array, and implicit float to int conversions that lose precision, such as$a[15.5]. - PDO with the MySQL and SQLite drivers now returns integers and floats as native types.
PHP 8.2: dynamic properties
- Dynamic properties are deprecated. Assigning
$obj->newField = 1on a class that never declarednewFieldlogs a deprecation. Declare the property. Use#[\AllowDynamicProperties]as a stopgap.stdClassis exempt. "${var}"string interpolation is deprecated. Write"{$var}"or"$var".utf8_encode()andutf8_decode()are deprecated. Usemb_convert_encoding().- Relative callables such as
"self::method"are deprecated. Use[self::class, 'method']. - Case functions such as
strtolower()anducfirst()now ignore the locale. Usemb_*functions for localized text. str_split('')returns an empty array instead of[""].
PHP 8.3: few breaks
range()is stricter. A step of0, a negative step on an increasing range, and infinity or NAN throwValueError.number_format()with negative$decimalsnow rounds before the decimal point.- A negative first array index changes the next implicit key:
$a[-5]then$a[]gives-4, not0. - Date errors now use
DateExceptionandDateErrorclasses instead of warnings. - Deprecated:
get_class()with no arguments,assert_options()and theassert.*INI settings, andReflectionProperty::setValue()with one argument.
PHP 8.4: the nullable parameter deprecation
- Implicitly nullable parameters.
function f(Foo $a = null)is deprecated. Write?Foo $a = null. Old libraries trigger this on every load, so update packages before you judge your own code. E_STRICTis gone andtrigger_error()withE_USER_ERRORis deprecated. Throw an exception instead.- CSV escape. Relying on the default
escapeargument offgetcsv(),fputcsv()andstr_getcsv()is deprecated. Pass it explicitly. - Moved to PECL: IMAP, OCI8, PDO_OCI and PSpell. If you use
imap_*functions, install the PECL package or switch libraries. - Session settings. Changing
session.sid_length,session.sid_bits_per_character,session.use_only_cookies,session.use_trans_sidand related settings is deprecated, as is theSIDconstant. round()throwsValueErrorfor an invalid mode, and the old pre-rounding was removed, so a few results change.- bcrypt cost defaults to 12 instead of 10. Logins take longer. Existing hashes still verify.
- OPcache JIT defaults changed. If you enabled JIT only through
opcache.jit_buffer_size, setopcache.jittoo. - New names can collide. Any new function or class in 8.4 breaks code that defines the same name. Wrap your own helpers in
function_exists(). - Library minimums rose. The manual lists libcurl 7.61.0, libxml2 2.9.4 and OpenSSL 1.1.1. Very old servers may not meet them.
Scan and fix commands
Run these on a workstation or in CI. Keep them off the web server.
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcompatibility/php-compatibility:"^10.0.0@dev"
vendor/bin/phpcs -i
# during the move: code must run on both versions
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 7.4-8.4 \
--ignore='*/vendor/*,*/node_modules/*' --report-full=compat-report.txt .
# after cutover: target only 8.4
vendor/bin/phpcs -ps --standard=PHPCompatibility --runtime-set testVersion 8.4 \
--ignore='*/vendor/*,*/node_modules/*' .
The version constraint above is the one the PHPCompatibility README showed on 22 Oct 2026. Check the README for the current one. PHPCompatibility 10 needs PHP_CodeSniffer 4.0.2 or newer.
Rector can apply some of the fixes. Review its diff before you commit it.
composer require rector/rector --dev
vendor/bin/rector # offers to create rector.php
vendor/bin/rector process --dry-run # preview
vendor/bin/rector process # apply
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src'])
->withPhpSets(); // reads the target PHP version from composer.json
Neither tool replaces staging tests. They read code and never execute it. Leave plugin and theme code you do not own alone and update it instead. On WordPress, see change the PHP version.
Last reviewed on 22 October 2026.