Most hosting control panels have a “PHP version” switch, and WordPress Site Health keeps recommending a newer one. Yet switching is still a common cause of white screens and the “There has been a critical error on this website” message.
The key to understanding what breaks, and when, is the difference between two stages: deprecated and removed. For this article I ran the same few lines of code under several PHP versions (7.0 through 8.5) installed on the shared server that hosts this blog.
Features disappear in stages
When PHP decides to retire a function or syntax, it usually goes through three stages:
- Works normally — no messages.
- Deprecated — still works exactly the same, but emits a
Deprecatednotice every time it runs. - Removed — the function no longer exists. Calling it is an error, usually fatal.
Deprecations are typically removed in the next major version (7 → 8). Here is the old array function each() on three versions:
/opt/php-<version>/bin/php -d display_errors=stderr -d error_reporting=-1 \
-r '$a=[1]; var_dump(each($a) !== false);'
| PHP | Output | Stage |
|---|---|---|
| 7.0.33 | bool(true) |
Works normally |
| 7.4.33 | Deprecated: The each() function is deprecated... then bool(true) |
Deprecated, same result |
| 8.0.30 | Fatal error: Uncaught Error: Call to undefined function each() |
Removed, execution stops |
each() was deprecated in PHP 7.2 and removed in 8.0. On 7.4 nothing visible changes on the site. On 8.0 the code stops at that line. That is the typical failure pattern: the warning was there for years, but nobody was reading it.
Why deprecation notices go unnoticed
On a production WordPress site, WP_DEBUG is usually false, so notices are neither displayed nor logged. Setting WP_DEBUG to true makes wp_debug_mode() in wp-includes/load.php call error_reporting( E_ALL ). To check a live site, log without displaying:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false ); // keep notices off visitors' screens
A deprecation log is effectively a list of the places that will break on the next major version. See wp-config.php key constants for details.
“Deprecated” does not always mean harmless
A deprecated feature behaves the same, but the notice itself is output, and that output can break something else.
We ran into this while building the maintenance tool behind this blog. With PHP 8.2+ and an older WP-CLI (before 2.8), WP-CLI’s own code printed lines like:
Deprecated: Creation of dynamic property WP_CLI\Dispatcher\CompositeCommand::$longdesc is deprecated in ...
WP-CLI finished its work correctly, but the tool parses WP-CLI output as JSON. With notices in front of the JSON, parsing failed and the plugin list could not be read. The behavior did not change, but the output did.
The tool now uses two layers: when WP-CLI is launched through php directly, it passes -d error_reporting='E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED', and if notices still appear, it extracts only the span from the first [ or { to the matching end. Suppressing notices protects the JSON, but it does not remove the deprecated code. The real fix is updating the source of the notice, and newer WP-CLI releases already address it.
On a WordPress site, a displayed notice can be printed before HTTP headers, followed by Cannot modify header information - headers already sent, which breaks redirects and logins.
What changed between versions
I ran four more snippets on PHP 7.0, 8.0, 8.1, 8.2 and 8.4 on the same server:
| Change | 7.0 | 8.0 | 8.1 | 8.2 | 8.4 |
|---|---|---|---|---|---|
0 == "abc" |
true |
false |
false |
false |
false |
strlen(null) |
silent | silent | Deprecated | Deprecated | Deprecated |
Dynamic property ($a->x = 1 on an empty class) |
silent | silent | silent | Deprecated | Deprecated |
Implicit nullable (stdClass $x = null) |
silent | silent | silent | silent | Deprecated |
each() |
works | removed (fatal) | removed | removed | removed |
- String-to-number comparison changed in PHP 8.0 with no notice at all. The same code returns a different result, so logs will not reveal it. These silent behavior changes are the hardest to catch.
- Passing null to built-in functions became deprecated in 8.1. Plugins that pass empty values straight into string functions often flood the log right after an upgrade.
- Dynamic properties became deprecated in 8.2, which caused the WP-CLI case above. WordPress core is adapting: in this blog’s WordPress 7.1.2, 130 files in
wp-includesandwp-admincontain the#[AllowDynamicProperties]attribute, which explicitly allows them while code migrates. - Implicitly nullable parameters became deprecated in 8.4. Write
?stdClass $x = nullinstead.
Minor versions (8.1 → 8.2) usually do not remove anything, but each one adds new deprecations, which become candidates for removal in a future major version.
When a fatal error happens
Since WordPress 5.2, a fatal error triggers recovery mode: the “critical error” message appears and the admin receives an email naming the plugin or theme, with a link to sign in and deactivate it. This only works if WordPress loads far enough. If wp-config.php or core itself fails, you get a blank page or an HTTP 500 (see HTTP status codes), and switching the PHP version back in the control panel is the quickest way out.
Where minimum versions are defined
- Core: in this blog’s WordPress 7.1.2,
wp-includes/version.phpsets$required_php_version = '7.4'. - Plugins and themes: since WordPress 5.3, the
Requires PHP:header is checked byis_php_version_compatible(), which isversion_compare( PHP_VERSION, $required, '>=' ).
Requires PHP is only a minimum. A plugin that requires PHP 7.4 may still emit notices or fail on 8.4.
Which PHP is actually running?
The PHP that serves your website and the PHP on the command line are not necessarily the same. On this server, wp --info over SSH reports /usr/bin/php at 8.0.30, while /opt holds versions from 7.0 to 8.5, and the web version is chosen per domain in the control panel. So php -v over SSH says nothing reliable about the web.
Our tool therefore checks the PHP version actually serving the site through the web server before updating WordPress, falls back to WP-CLI’s PHP only if that fails, records which source was used, and does not proceed if the version is below WordPress’s requirement. The reverse also matters: after switching the site to 8.4, cron jobs and WP-CLI may still run the old PHP.
A safe upgrade order
- Back up files and the database (see the 3-2-1 backup rule).
- Log deprecations on the current version with
WP_DEBUG_LOGfor a while. - Update plugins, themes and core first. Many deprecations are fixed upstream; replace abandoned plugins.
- Switch on staging and test key pages, forms and admin tasks. Static analysis such as PHP_CodeSniffer with PHPCompatibility rules can list removed functions.
- Switch production and check immediately, including
debug.logfor new fatal errors and deprecations. - Avoid large jumps. Going from 7.4 straight to 8.4 makes it hard to tell which change broke what.
- Turn
WP_DEBUGback off when you are done.
Summary
PHP features retire in stages: works → deprecated (same behavior, plus a notice) → removed (fatal error). On this server, each() ran silently on 7.0, warned but worked on 7.4, and stopped execution on 8.0. Deprecation notices are often discarded in production, yet they are a list of what will break next, and the notices themselves can corrupt JSON or HTTP headers. Back up, read the deprecation log, update plugins first, test on staging, then switch, and make sure the PHP you checked is the one actually serving your site.