The 2020 WordPress and PHP 8 compatibility report

On November 26, PHP 8 will be released to the world. PHP 8 is set to become one of the most breaking PHP releases in the history of PHP and will bring along unprecedented challenges for legacy PHP codebases, like WordPress, to fix compatibility. 

Today we bring you a comprehensive report on WordPress and PHP 8 compatibility. In sharing this, we hope to educate and help inform both the WordPress and PHP communities about the state of WordPress and PHP 8. After all, PHP is the technology that powers WordPress and WordPress is by far the largest consumer of PHP.

This report is a team effort by Juliette (a PHP engineer well-respected in both the PHP and WordPress communities), Herre (Chief software architect at Yoast) and myself. 

Introduction

What’s in this report?

In the first part of this report we’ll outline the changes in PHP 8 that are likely to significantly impact WordPress and other legacy codebases. In the second part of this report, we’ll try to provide a perspective on past, present and future challenges regarding WordPress and PHP compatibility. At the end of the report we’ve included a case study of Yoast.com, to illustrate what kind of issues are likely to occur for a large WordPress site on PHP 8. 

How come there are so many breaking changes in PHP 8?

PHP 8 is a major update of PHP and it is common practice to remove deprecations in major versions from the previous range of minor versions. For PHP 8, many of the breaking changes have been deprecated in previous 7.* versions. So for projects that were diligently updated over the years fixing their deprecated API’s, it shouldn’t be hard to upgrade at all. 

However, PHP 7.* versions have seen a far larger set of deprecations than previous versions of PHP. Where PHP 5.6 to PHP 7 was a relatively simple migration, going from 7.x to 8 could be very painful, especially for very old codebases, like WordPress and many of the plugins that are available for it. For well-typed codebases or codebases which have stayed up-to-date with the latest PHP versions, there isn’t a big problem. The reality, however, is that WordPress is not such a codebase.

Isn’t WordPress already compatible with PHP 8?

Well… Yes. Sort of. Maybe. We are highly doubtful. It’s really not possible to tell.

WordPress aims to always be compatible with new versions of PHP. Sergey did an amazing job in fixing most of the compatibility issues that could be detected using the available strategies. We’ll definitely dive deeper into what those are and the issues that exist with them. Technically, the compatibility of the current nightly of WordPress with PHP 8 is at the same level as we’re used to from WordPress releases right before a new version of PHP comes out. We believe the testing was as extensive, the fixing was as diligent and the level of fixes was as high as any round of PHP compatibility fixing within WordPress core. See also the call for testing on PHP 8 on WordPress.org.

However, doing what we’ve always done, unfortunately, will not cut it this time. The sheer amount of breaking changes and the type of changes included in PHP 8, plus some added complexities in cross-version tooling, make this compatibility challenge a different beast from what we’ve seen before. This report aims to explain how that is the case.

Part 1: The most worrisome breaking changes in PHP 8

Strict typing on internals in PHP 8

One of the most important breaking changes in PHP 8 has to do with strict typing. User-defined functions in PHP already throw a TypeError. However, internal functions emitted warnings and returned null. PHP 8 makes this consistent and internal functions now also throw a TypeError. This will not only impact functions that already threw warnings prior to PHP 8, but also magic methods (which previously weren’t type checked) and functions that have had type declarations introduced. For this reason, it’s not possible to catch all issues that arise from this change by fixing the type warnings in PHP 7.4 environments. Below is an overview of related breaking changes that together define the scope of strict typing related changes in PHP 8.

Consistent type errors

As of PHP 8 internal functions now throw a TypeError for all typed arguments.

Arithmetic operator type checks

The arithmetic and bitwise operators +, -, *, /, **, %, <<, >>, &, |, ^, ~, ++, — will now consistently throw a TypeError when one of the operands is an array, resource or non-overloaded object. The only exception to this is the array + array union operation, which remains supported.

Before PHP 8, it was possible to apply arithmetic or bitwise operators on arrays, resources or objects. This isn’t possible anymore, and will throw a TypeError.

Magic methods type checks

Magic Methods will now have their arguments and return types checked if they have them declared. The signatures should match the following list:

  • __call(string $name, array $arguments): mixed
  • __callStatic(string $name, array $arguments): mixed
  • __clone(): void
  • __debugInfo(): ?array
  • __get(string $name): mixed
  • __invoke(mixed $arguments): mixed
  • __isset(string $name): bool
  • __serialize(): array
  • __set(string $name, mixed $value): void
  • __set_state(array $properties): object
  • __sleep(): array
  • __unserialize(array $data): void
  • __unset(string $name): void
  • __wakeup(): void

Numeric string handling

Numeric string handling changed to be more intuitive and less error-prone. Trailing whitespace is now allowed in numeric strings for consistency with how leading whitespace is treated. This mostly affects:

  • The is_numeric() function
  • String-to-string comparisons
  • Type declarations
  • Increment and decrement operations

The concept of a “leading-numeric string” has been mostly dropped; the cases where this remains exist in order to ease migration. Strings which emitted an E_NOTICE “A non well-formed numeric value encountered” will now emit an E_WARNING “A non-numeric value encountered” and all strings which emitted an E_WARNING “A non-numeric value encountered” will now throw a TypeError. This mostly affects:

  • Arithmetic operations
  • Bitwise operations

This E_WARNING to TypeError change also affects the E_WARNING “Illegal string offset ‘string'” for illegal string offsets. There are no changes in the behavior of explicit casts to int/float from strings.

Named parameters

Support has also been added for named parameters. This has two major implications:

  1. Renaming parameters becomes a breaking change. If a parameter is renamed then anywhere that function is called with named parameters will break.
  2. The behaviour of call_user_func_array() changes. Previously call_user_func_array() could be called with an associative array. Now passing an associative array will be interpreted as using named parameters, which will cause an Exception to be thrown, if any of the named parameters do not exist.

API changes which could lead to type errors

Below we’ve compiled a list with some examples of API changes that will lead to type or argument errors where there were no indications as such in previous PHP versions.

  • mktime() and gmmktime() now require at least one argument. time() can be used to get the current timestamp.
  • spl_autoload_register() will now always throw a TypeError on invalid arguments, therefore the second argument $do_throw is ignored and a notice will be emitted if it is set to false.
  • assert() will no longer evaluate string arguments, instead they will be treated like any other argument. assert($a == $b) should be used instead assert(‘$a == $b’). The assert.quiet_eval ini directive and the ASSERT_QUIET_EVAL constant have also been removed, as they will no longer have any effect.
  • The $args argument of vsprintf(), vfprintf(), and vprintf() must now be an array. Previously any type was accepted.
  • Arguments with a default value that resolves to null at runtime will no longer implicitly mark the argument type as nullable. Either use an explicit nullable type, or an explicit null default value instead.

Warnings converted to error exceptions

There are a large number of PHP warnings that have been changed to error exceptions in PHP 8. 

Error level changes unrelated to RFC’s

The following warnings have been converted to errors probably related to deprecations in PHP 7.x versions:

  • Attempting to write to a property of a non-object. Previously this implicitly created an stdClass object for null, false and empty strings.
  • Attempting to append an element to an array for which the PHP_INT_MAX key is already used.
  • Attempting to use an invalid type (array or object) as an array key or string offset.
  • Attempting to write to an array index of a scalar value.
  • Attempting to unpack a non-array/Traversable.

Reclassified engine warnings

Lots of errors that previously only triggered warnings or notices, have been converted to errors. The following were changed:

  • Undefined variable: Warning instead of notice
  • Undefined array index: warning instead of notice
  • Division by zero: DivisionByZeroError exception instead of warning
  • Attempt to increment/decrement property ‘%s’ of non-object: Error exception instead of warning
  • Attempt to modify property ‘%s’ of non-object: Error exception instead of warning
  • Attempt to assign property ‘%s’ of non-object: Error exception instead of warning
  • Creating default object from empty value: Error exception instead of warning
  • Trying to get property ‘%s’ of non-object: warning instead of notice
  • Undefined property: %s::$%s: warning instead of notice
  • Cannot add element to the array as the next element is already occupied: Error exception instead of warning
  • Cannot unset offset in a non-array variable: Error exception instead of warning
  • Cannot use a scalar value as an array: Error exception instead of warning
  • Only arrays and Traversables can be unpacked: TypeError exception instead of warning
  • Invalid argument supplied for foreach(): TypeError exception instead of warning
  • Illegal offset type: TypeError exception instead of warning
  • Illegal offset type in isset or empty: TypeError exception instead of warning
  • Illegal offset type in unset: TypeError exception instead of warning
  • Array to string conversion: warning instead of notice
  • Resource ID#%d used as offset, casting to integer (%d): warning instead of notice
  • String offset cast occurred: warning instead of notice
  • Uninitialized string offset: %d: warning instead of notice
  • Cannot assign an empty string to a string offset: Error exception instead of warning
  • Supplied resource is not a valid stream resource: TypeError exception instead of warning
  • #The @ operator no longer silences fatal errors
    It’s possible that this change might reveal errors that again were hidden before PHP 8. Make sure to set display_errors=Off on your production servers!

Fatal error for incompatible method signatures

Inheritance errors due to incompatible method signatures between two classes will now throw a fatal error instead of a warning.

Default error reporting level

With PHP 8 the default error reporting level is changed to E_ALL instead of everything but E_NOTICE and E_DEPRECATED. This means that many errors will start showing up which were previously silently ignored.

7.x deprecations

During the PHP 7.x release cycle, each version introduced new deprecations, which have now been finalized as feature removals in PHP 8. This also applies to some deprecation which were already put in place in PHP 5.x, but weren’t removed in the PHP 7.0 release.

Most notably, the following, already deprecated features, have been removed in PHP 8:

  • $php_errormsg
  • create_function()
  • mbstring.func_overload
  • parse_str() without second argument
  • each()
  • assert() with string argument
  • $errcontext argument of error handler
  • String search functions with integer needle
  • Defining a free-standing assert() function
  • The real type (alias for float)
  • Magic quotes legacy
  • array_key_exists() with objects
  • Reflection export() methods
  • implode() parameter order mix
  • Unbinding $this from non-static closures
  • restore_include_path() function
  • allow_url_include ini directive

Other breaking changes.

There are many other (breaking) changes in PHP 8, including significant changes to the return types of select, but often-used, functions, resources being turned into Value Objects and more. Examples for GD, OpenSSL, Sockets, XML, Zlib, substr() et al.

Above we’ve tried to highlight the ones that are likely to directly impact WordPress (and many other legacy systems) in a significant way. We’ve based this overview on the excellent guide “What’s new in PHP 8” on stitcher.io and the PHP 8 upgrade guide. For more information we refer you to those sources.

Part 2: Compatibility challenges

To make an existing codebase compatible with a new version of PHP, there are a couple of different strategies that can be deployed for discovery:

  • Static analysis tools like PHPCompatibility to detect syntactic issues.
  • Automated testing to detect runtime issues.
  • Manual testing to detect runtime issues.

Depending on the coverage of your test suite and the proportion of runtime and syntactic changes, these strategies serve as a good basis for fixing the compatibility of a codebase with a new version of PHP. However, in the case of WordPress and PHP 8, there are quite a few extra challenges which makes it hard to rely on these strategies for fixing compatibility and declaring WordPress compatible with PHP 8. Below we’ll report on which strategies have been deployed for WordPress and what the results were.

Static analysis tools

Due to the nature of some of the changes in PHP 8.0, the issues which can be found using static analysis are limited. And in those cases where static analysis tries to go beyond their traditional capabilities and tries to trace the runtime type and value of variables and constants, the results of such scans will be very prone to false positives.

Aside from that, PHPCompatibility is the only static analysis tool dedicated to finding issues related to PHP cross-version compatibility.

Other static analysis tools will report on a far larger scope of issues. Wading through the results to find the issues which are related to PHP cross-version compatibility and actually correct, is very time-consuming and requires in-depth knowledge of the tooling to configure them for the least amount of noise.

At the same time these tools are in constant flux, trying to keep up with the changes in PHP and updating the available scans, so independently of what has been and can be found at this time, chances are that these tools will find still more issues in the (near) future.

Scanning WordPress with PHPCompatibility

Issues reported early on in the WP 5.6 dev cycle and fixed since based on PHPCompatibility scans:

Another PHP 8 issue detected by PHPCompatibility is “__destruct() will no longer be called after die() in __construct()”. This is correctly detected by the scanner, but upon further analysis has been determined not to be problematic in this particular case.

PHPCompatibility has also detected an issue in code used by the Plugin/Theme editor. Analyses of the involved code has determined there is an underlying oversight in the code. WordPress tries to do minimal analysis of the code in the editor, but doesn’t take PHP 5.3+ (namespaced) code into account. This oversight will now just be made more complex to solve while taking related changes in PHP 8.0 into account.

Scans with PHPCompatibility have been run with the develop version. No version has been released yet containing the PHP 8 specific scans.

Issues detected by the scanner in externally maintained dependencies have been reported there.

Scanning WordPress with Exakat

As of the latest public scan, based on WP trunk of October 16th, Exakat reports a total of 149.567 issues.

Exakat dashboard screenshot

The PHP 8 compatibility report contains a total of 93 issues, but is incomplete as a number of analyses relevant for PHP 8 are not (yet) included in the report.

Exakat report

The “worst” offender based on the PHP 8.0 compatibility report, is parameters in method declarations in child classes being named differently from the parameter in the parent class. This is incompatible with the new PHP 8.0 “named parameters in function calls” feature.

This has been reported in issue Trac 51553 and a patch for this has been attached to the ticket, but has not yet been committed.

Other tasks which ought to be executed to prepare for named parameters in function calls have been listed in this ticket as well, including an action list for one of these tasks. No action has been taken on any of these so far.

The 12 “Unsupported Types with Operators” warnings are mostly false positives and this has been reported to Exakat.

More worrisome are the 14.679 issues reported for using the wrong argument type, 14.135 issues for wrong type with call, 15.605 issues reported for wrong number of arguments, 801 wrong type for native PHP function and 25 wrong parameter type issues.

While it is expected that these reports will contain a large number of false positives as WordPress doesn’t use type declarations and the types are therefore extrapolated from the code found and the types indicated in docblocks, these issues should still be examined individually. Even if only just 1% of the found issues is correct, that would still come down to ~450 errors which still need to be dealt with. Quite apart from the huge amount of time needed to weed out the real issues from the false positives. At the time of writing, the author is not aware of any efforts being made to examine these reports and identify and fix these issues.

A few of the analysis reports by Exakat for other issues related to PHP 8, not contained in the PHP 8.0 compatibility report, have been examined. Patches for these have been submitted and committed over the last week. This includes:

  • A fix for a PHP 8.0 fatal error in the WP Revisions module.
  • A fix for a PHP 8.0 warning in the pomo library.

Scanning WordPress with PHPStan

Scans with PHPStan need a highly customized ruleset to get remotely usable results and even then, they are riddles with false positives, making the output neigh unusable.

Note: this is not necessarily criticism of the PHPStan tooling, but is largely due to the fact that WordPress barely uses type declarations, while PHPStan is primarily geared towards projects using modern code.

An initial scan with the most basic of configurations, yields well over 20.000 issues.

A scan with the above mentioned, highly customized ruleset, aimed at PHP 8 related issues specifically, still yields 580 issues at level 5, another 2.150 potential issues at level 7, though these will likely contain a lot of false positives and yet 380 more issues at level 8 with the same caveat.

A Trac ticket was opened a while back to address a list of issues based on an unknown configuration, but specifically aimed at passed parameter type mismatches (level 5). There is a draft PR available to fix these issues.

An initial assessment of this PR, however, shows that the majority of fixes proposed would hide issues by typecasting variables to the expected type, not actually fix them by doing proper type checking. This can lead to unexpected behaviour in the application if these changes are not accompanied by strict unit tests (and they are not). This will also likely result in much harder to debug errors further down the line.

At this time, it has not been verified whether the fixes proposed are even warranted or that the issues identified should be considered false positives.

Scans run with PHPStan 0.12.52.

Testing

Due to the nature of the problematic changes in PHP 8.0, static analysis can only go so far. Manually reviewing and testing software is painstaking work and humans are very prone to overlook things when there is a lot to be looking out for.

Manually testing performed by end-users tends to be relatively useless, as this will generally only result in “happy paths” being tested. Comprehensive exploratory and regression testing is needed to achieve more reliable results. And even if problems are found, it requires extensive debugging to figure out the cause of the problem – WordPress ? Plugin Theme ? – and whether it is related to PHP compatibility.

More than anything it is important to have automated tests of good quality and to run these on PHP 8, as this will give the best indication of PHP 8.0 problems to expect.

Running automated tests on PHP 8

Getting an automated test suite to run on PHP 8 takes us down the next rabbit hole as the de facto tool for unit testing in the PHP world, PHPUnit, generally does a major release every year and with each major drops support for older PHP versions and introduces breaking changes. The first PHPUnit version which is officially compatible with PHP 8.0 is PHPUnit 9.3, released August 2020.

As WordPress still supports PHP 5.6 as a minimum, to run tests on PHP 8.0, any WordPress related test suite will have to be compatible with PHPUnit 5 up to PHPUnit 9.

While tooling is being built to help with that (look out for a blogpost about this over the next week!), it still takes time and effort to implement these tools and make a test suite compatible. Time which is taken away from the time available to actually fix PHP 8 related problems.

Getting the tests to run on PHP 8 for WP Core

The tests for WP Core are run against PHP 8 and are currently passing. The tests are being run on a composer installed version of PHPUnit 7.5, even though PHPUnit 9.3 is the earliest PHPUnit version officially compatible with PHP 8.0.

This last point has been overcome by copying a select number of files/classes from PHPUnit 9.3 to the WP test suite, excluding the PHPUnit native classes from the Composer autoload generation, in favour of using the copies from PHPUnit 9.3 in the WP test suite. While this works for now, this is a hacky solution and may not be sustainable in the future, aside from the maintenance it may currently require.

Note: while all other WordPress Core CI builds use the Phar version of PHPUnit, this is not possible when running PHPUnit 7.5 on PHP 8.0 as PHPUnit 7 is no longer maintained and the Phar contains incompatible dependencies of PHPUnit. Using a Composer based install of PHPUnit overcomes this as it will pull in the latest compatible versions of the dependencies, though –ignore-platform-reqs is needed.

As for the quality of the tests, this was relatively low to begin with, with loose type checking being used in the majority of cases.

A Trac ticket to address this was already opened in 2016. With an eye on the stricter type adherence in PHP 8.0, this ticket has been revived and a lot of work has been done to mitigate this.

At the time of writing, there are nearly 800 instances (676 assertEquals() + 96 assertNotEquals()) still using loose type checking – down from over 8000 instances.

In part the remaining loose type assertions are legitimate when objects are being compared, in part these still need to be addressed, but would currently result in test failures. These last ones highlight shortcomings either in the tests, but more often in the code being tested.

Code Coverage

When evaluating the value of the current test suite, it is important to look at the code coverage of the test suite and to understand how code coverage works.

Code coverage can be reported in two ways: 

  • Unclean
  • Clean (strict)

To explain the difference and why this difference is important, we take a simple code sample: 

function bar() {
    // Do something
}

function foo() {
    $bar = bar();
    // Do something more.
}

Now, if there are tests for method foo() and code coverage is not set up to be strict, the code coverage for the above code will be reported as 100% as both methods are called when the tests are being run.

In contrast, if @covers tags are used, the code coverage for the above code would be reported as 50% (only method foo()).

When finding PHP 8.0 related issues, this difference is important as a method with dedicated tests will – if things are done correctly – be “stress tested”, i.e. tested with different types and values of input, making sure the method handles unexpected situations correctly.

In contrast, the method which does not have dedicated tests, but is “incidentally” covered by a test, will only have been tested for the “happy path”, i.e. giving a result so the test for the method really being tested, can continue.

As most new fatal errors in PHP 8.0 are related to the “unhappy path”, it is important to have dedicated tests for all code and for these dedicated tests to cover both the “happy path”, as well as the “unhappy path”.

By improving the test suite to use @covers tags, it will become clear which part of the code base does not have dedicated tests. This will allow for setting informed priorities for expanding the test suite, starting with the most important parts of the code base which don’t have dedicated tests.

Four years ago, an issue was already opened to draw attention to the missing @covers tags. This dormant ticket has also been revived with an eye on PHP 8 and a number of patches to add @covers tags have been submitted over the past few weeks. None of these have been committed so far.

At this time, WordPress has mostly “unclean” code coverage. A scan in August of this year showed an unclean code coverage of 39.82%. A scan this week, showed an unclean code coverage of 40.32%, which is a 0.5% improvement.

Based on previous experiences, an unclean code coverage of 40% will likely translate to approximately 15 – 20% real code coverage, leaving 60% of the WordPress Core code currently completely untested and another 20% or more only tested for the “happy path”. When tied to the enormous amount of breaking changes in PHP 8, this becomes highly concerning. Basically automated testing as a strategy for detecting compatibility issues cannot be relied upon.

Note: a strict code coverage run which only records coverage for those tests with @covers tags in the WordPress test suite, currently shows a 6.8% code coverage, so that can be taken as the absolute minimum.

Testing plugins and themes

Only a small percentage of the available plugins, the more popular and professionally developed ones, have automated tests in place. This is worrisome as generally speaking, the average WordPress site runs about 19 or 20 plugins. Quite a few sites have more plugins running though.

For themes, having automated testing in place, is even more rare.

Allowing for these test suites to run on PHP 8.0 is challenging and that’s even before insight can be gained about the PHP 8 compatibility of these plugins and themes.

However, more than anything, the plugins/themes which have tests are likely the ones where the least amount of PHP 8.0 problems can be expected as they use a professional development model.

The multitude of plugins and themes without tests are much more cause for concern as these will more likely to be problematic when run on PHP 8.

For plugins (and themes), which do have tests, there are primarily two types of tests they may or may not have in place:

  • Integration tests. These are tests where WordPress itself is loaded before running the test suite and which will use the WordPress core code and integrate with the WordPress test suite.
  • Unit tests. Stand-alone tests which “mock” WordPress to allow for testing the plugin code, often using popular frameworks like Mockery or BrainMonkey.

Integration tests

As WordPress itself has decided to stick to PHPUnit 7.5, this means for integration tests for plugins and themes, that those will also be bound to PHPUnit 7.5 (maximum).

Plugins and themes will either need to copy the hack in WordPress core to get their integration tests running or alternatively, they need to use the files in WordPress Core, but will then need to create a custom autoloader as they cannot use the same Composer autoload generation hack.

Such a custom autoloader will need to be bootstrapped before the Composer autoload file to prevent the PHPUnit native files from loading anyway.

Unit tests

For unit tests using BrainMonkey or Mockery, PHPUnit > 8 is needed as the Mockery framework (also used by BrainMonkey) available for PHPUnit 7.x is not compatible with PHP 8.0. This means that these test suites will have to be made compatible with PHPUnit 5 up to 9. This also adds another challenge, as when both types of test suites are being used, different versions of PHPUnit are needed to run each test suite.

To aggravate this situation, plugins will generally have a committed composer.lock file to make sure their runtime dependencies are at a certain version they can depend on and which is compatible with PHP 5.6. This last part is often enforced by having a platform: php 5.6 type of configuration in their composer.json file. However, that also means their dev-dependencies, like PHPUnit, BrainMonkey, Mockery, will also be locked at a version which is compatible with PHP 5.6, which will prevent the tests from running on PHP 8.0.

This can be overcome by on-the-fly removing the platform setting and updating the composer.json and composer.lockfiles, but this does make running the tests on PHP 8.0 yet more involved, both in CI, as well as locally for its developers, let alone for outside contributors.

Compatibility of external WordPress dependencies

Part of WordPress compatibility with PHP 8 is also determined by its external PHP dependencies. While the latest nightly of WordPress seems to be compatible with PHP 8 based on the strategies listed above (and with the necessary caveats), this cannot be said for the external dependencies yet.

External dependencies (maintained)

At this time, the PHP 8 compatibility status of external dependencies or WordPress Core, like GetID3, PHPMailer and Requests, is unknown.

  • GetID3 has precisely 1 test for the unreleased 2.0 branch. This test is not run in CI. Code coverage is basically 0%. A number of PHP 8 related fixes based on static analysis discovery have been pulled and merged, but are not contained in a tagged release yet.
  • PHPMailer has tests (+/- 73% unclean code coverage), but these aren’t run against PHP 8 yet. Contact has been established with the maintainer of the library and movement on this is expected over the next week or so. So far, one PHP 8 issue has been reported and fixed. The fix is not available yet in a tagged release.
  • Requests has tests (+/- 90% unclean code coverage). While the tests are being run against PHP 8, they are failing due to the PHPUnit version being used not being compatible with PHP 8. The actual PHP 8 status won’t be known until the test suite is fixed. The project hasn’t had a release since 2016. The maintainer has largely abandoned the project, but is liberal with giving out commit rights to anyone who complains about it. Two of these “complainers” are currently collaborating to at least get a new version tagged, as well as getting the tests properly running on PHP 8, but this is a temporary situation born out of pure desperation and the abandonment of this repo by the maintainer is still a point of concern.
  • SimplePie has tests, though the code coverage is unknown. The tests are being run on PHP 8. A number of PHP 8 related fixes have been pulled and merged and are included in the 1.5.6 release. WordPress has updated to SimplePie 1.5.6. There is currently one unaddressed test failure on PHP 8.
  • Random_Compat and Sodium_Compat are not much of a concern. Aside from these being very well tested, the code in these compat layers will not be called when PHP 8.0 is used. All the same, Sodium_Compat has been updated to the latest version to address a PHP 8 issue in the bootstrap.

Based on the above, it can only be hoped for that new releases of most of these external dependencies are forthcoming over the next few weeks/months containing PHP 8.0 compatibility fixes. This also means that WordPress will still need to update these external dependencies once the new releases become available, even though WP has entered the beta stage for WP 5.6 already.

External dependencies (unmaintained)

Other external dependencies, like pomo, PCLZip, Snoopy, RSS, are no longer maintained externally and the burden of making these compatible with PHP 8.0 falls on WP itself.

Most of these, with the exception of pomo, are not tested in the WP testsuite and even explicitly excluded from code coverage and other quality checks, making their PHP 8 status another unknown.

Also, as these external dependencies are “old” code, which has barely seen any maintenance over the past few years aside from fixing select PHP cross-version compatibility issues based on static analysis, this code should be considered PHP 4-code and approached with caution and trepidation for more problems to come.

Impact on broader ecosystem

While each of the above listed issues individually is of small concern, the accumulation of these all together is cause for serious concern about the compatibility of WordPress with PHP 8.0 for the foreseeable future.

To top that up, WordPress is never run as a stand-alone product, but always accompanied by a theme and some plugins. The extensibility of WordPress has been a large factor in its success, but also poses hard to overcome extra challenges in terms of compatibility.

The non-type safe use of broadly relied upon functions like apply_filters() and unprotected global variables will be a pain for years to come, causing fatal errors which will be mis-identified as belonging to Core or plugin B, while caused by plugin A. This is obviously a major cause of concern and is likely to bite a lot of end-users.

Part 3: yoast.com case study

To the point of plugin A causing plugin B or even WordPress core to break, we decided it would be good to run an analysis on yoast.com. In order to get a good overview of the impact PHP 8 would have on a large WordPress site we’ve compiled a list of all PHP warnings currently being thrown on yoast.com that, on PHP 8, would cause fatal errors. It’s important to note that this only covers a small portion of the breaking changes, but it gives a nice indication of the impact these changes could have on a live site.

What did we do?

Via the PHP_FPM module for Elastic beats we logged all PHP warnings and errors straight into our Elastic database. We decided to compile a list of warnings that occured in the last 30 days and filter that list down to the warnings that would raise errors in PHP 8. We got the following list as a result:

Error message Type Count
Creating default object from empty value Theme 266,413
ltrim() expects parameter 1 to be string, array given WP 131,666
count(): Parameter must be an array or an object that implements Countable Plugin 7,129
Invalid argument supplied for foreach() WP 4,685
A non-numeric value encountered Plugin 3,072
A non-numeric value encountered Plugin 3,072
A non-numeric value encountered Plugin 2,981
Division by zero Plugin 1,288
array_filter() expects parameter 1 to be array, null given Plugin 830
call_user_func_array() expects parameter 1 to be a valid callback, class ‘HelpScout_Docs_API\Admin’ does not have a method ‘insert_post’ WP 398
Illegal offset type in isset or empty WP 267
A non-numeric value encountered Plugin 184
A non-numeric value encountered Plugin 100
count(): Parameter must be an array or an object that implements Countable Plugin 97
count(): Parameter must be an array or an object that implements Countable Theme 53
A non-numeric value encountered Site 50
trim() expects parameter 1 to be string, array given WP 38
count(): Parameter must be an array or an object that implements Countable Plugin 28
A non-numeric value encountered Plugin 19
Illegal string offset ‘full’ Site 13
A non-numeric value encountered Plugin 9
Invalid argument supplied for foreach() Plugin 7
Invalid argument supplied for foreach() Plugin 4
Illegal string offset ‘usp’ WP 3
count(): Parameter must be an array or an object that implements Countable Plugin 2
Invalid argument supplied for foreach() Plugin 2
strlen() expects parameter 1 to be string, array given Plugin 2
date() expects parameter 2 to be int, string given Site 2
date() expects parameter 2 to be int, string given Site 2
A non-numeric value encountered Plugin 1
array_keys() expects parameter 1 to be array, null given Plugin 1
Invalid argument supplied for foreach() Plugin 1

Analysis

As you can see we got a list that ranges from warnings that occur over 250.000 times a month in our theme to warnings that occurred only twice (during PayPal checkouts). In total there were nearly half a million warnings of 32 different types that, on PHP 8, would cause fatal errors.

Warnings coming from anywhere

Of those 32 types of warnings we found 6 that occured in code that’s specific to yoast.com and thus wouldn’t occur on any other site. 20 different types originate directly from 8 different plugins. The remaining 6 occurred in WordPress core code although all of them due to the interaction with plugins. The example of plugin A breaking WordPress core checks out.

From frequent to rare

Of all these warnings, there are two that occur extremely frequently, over 100.000 times in the last month. One originating from our own theme and one due to the esc_url() function being called with an array instead of a string. As we start testing yoast.com on PHP 8 these are likely easily found and just as easily fixed. It won’t take much effort to reproduce these. A simple smoke-test on a staging site would find these errors.

Six types occur less frequently, more than 1.000 times but less than 10.000 times a month. All of these originate from plugins and with 8 million requests served by our servers each month thus occur in less than 1 in 1.000 requests. These are errors that likely wouldn’t all be found during a quick test on a staging environment but would call for much more intensive testing to be reliably caught. Given their frequency it’s likely that these would cause disruption on most sites and if these occur during, for example, AJAX requests they could be much harder to detect. Especially on e-commerce sites users may not report these errors and simply abandon their cart.

Five other types occur between 100 and 1.000 times. All of these come either from plugins or from how plugins are interacting with WordPress core. Given their frequency, it’s unlikely that all of these would be found while testing a site on PHP 8. They are frequent enough that there likely will be users reporting issues eventually but this may take some time due to their rarity. Similar to the issues above if these occur during hidden requests they could be much harder to detect.

Finally we have 19 types of warnings that occur less than 100 times a month. Five of these are specific to yoast.com with the remaining 14 originating from plugins or their interactions with WordPress core. It’s entirely possible that, even with intensive testing, none of these would be found. These are warnings that occur in less than 1 in 100.000 requests, 10 of them even occur less than 1 in 1 million requests. The disruption these would cause is likely to be minimal, although it should be noted that 4 of these occur during checkout and thus would impact a few very specific users more severely. These are errors that due to their rarity may go undetected for months.

In conclusion, when migrating a WordPress site to PHP 8.0 it’s likely that a small number of very frequently occurring issues will be found easily and quickly and fixing these will account for 95% of total errors. With more intensive testing it should be very possible to account for 99% of all errors but based on the data on yoast.com it’s likely that at this point less than half of the different causes will have been found. With the remainder being issues that occur extremely rarely but may happen in some very unfortunate places, such as our checkout. 

Things are looking tricky for PHP 8 compatibility on large WordPress sites

By just investigating a subset of breaking changes in PHP 8 we could already confirm this is likely to cause major breakage on sites with unclear origin of that breakage. Oftentimes the error will occur in one place, but is caused by a plugin or theme in a different place, making these issues hard to debug.

Yoast.com is obviously an actively maintained WordPress site, supported by a team of professional developers. The large majority of WordPress sites doesn’t have this luxury and it will not be easy to mitigate compatibility issues on these sites. 

Conclusion

PHP 8 is going to contain a lot of breaking changes. We’ve described a subset of those changes in this report; the ones which we assume will have the biggest impact on WordPress and the broader WordPress ecosystem. Those mainly have to do with warnings becoming errors and many many strict type related errors that are being introduced. A rather high portion of these changes can only be detected on runtime.

Fixing these compatibility issues is a major task and to do so you need to use different strategies, ranging from static analysis to automated testing. To do this properly the right tools are required. For projects like WordPress which need to support a range of PHP versions, many extra complexities are introduced in juggling around different versions of the analysis tools. This becomes increasingly hard, partly due to the syntactic and runtime differences between PHP 5.x and 8.x being so incredibly big. This is not to argue whether supporting PHP versions that far back is a good idea or not, the only conclusion here is simply that it becomes increasingly hard to do so.

We’ve also looked at the problem of coverage and WordPress’s PHP dependencies. High test coverage is necessary to reliably detect compatibility and it is even more important with PHP 8, where there are many more compatibility issues than usual and a lot of them can only be detected on runtime. In that sense, it’s hard to say what WordPress core compatibility with PHP 8 truly is, since test coverage is low and virtually absent for dependencies.

Because of PHP 8 focusing so strongly on strict typing, WordPress’ type unsafe extensibility system also becomes extra vulnerable to errors, potentially leading to plugins causing type errors in other plugins or WordPress itself. To put this to the test, we ran an analysis on error data from yoast.com from the last month. As a large site with a shop running on it, we figured it might give a good indication of the types of problems we can expect. Indeed we found many warnings that will turn into errors with PHP 8.

One final note we would like to make, is that WordPress isn’t the only legacy codebase out there and also not the only project which aims to support a wide range of PHP versions. The information in this article might thus well apply to other projects too.

We will not include any further call to action. The primary goal of this post is to inform and create an overview of the problems and challenges related to PHP 8 compatibility in WordPress. We sincerely hope it serves this purpose well. 

Updated 11-11: changed the sentence about how many plugins a site runs. It previously said, in error, that that was 30 to a 100. After consulting our tracking data we’ve concluded that the average is in fact 19.3 and thus 19 to 20 plugins.

Coming up next!