Common PHP v8.1 Compatibility Fixes for WordPress

It’s that time of year again. The end of life for PHP 7.4 is on November 28th, 2022. If you haven’t upgraded your WordPress website to PHP 8 or 8.1, now is the time. At Anchor Hosting we make sure all customer’s websites are running a supported version of PHP. I’m happy to report that nearly all customers running PHP 7.4 has been upgraded to PHP v8.1. I plan to upgrade and patch the remaining customers for PHP 8.1 before Nov 28th.

Upgrading to PHP 8 was intense 😅. My initial bulk upgrade attempt found around 300 sites out of 1500 not ready for PHP 8, which required manual efforts to resolve a wide range of PHP fatal errors. While deploying updates here at Anchor Hosting I’ve compiled a list of common compatibility fixes that were necessary for PHP 8.x. Before running PHP upgrades, here are a few suggestions.

  • Run all theme and plugin updates. Most problems you’ll run into with PHP 8.1 are due to outdated themes and plugins. Also, look for unlicensed add-ons and get them properly licensed. Unlicensed add-ons typically do not receive updates.
  • Upgrade all staging environments first. This is a great way to catch major issues, like a full site crash. Anchor Hosting uses Kinsta which doesn’t have a bulk PHP update option. That said I was able to reach out to Kinsta support and have them bulk update any staging environment running PHP 7.4 to 8.1. This was a huge time saver.
  • Upgrade early not late. It pays to upgrade things early and not wait until the deadline. If you wait too long you won’t have the option to revert back to PHP 7.4. Kinsta will be disabling PHP v7.4 as an option beginning on November 29th, 2022 for live sites. That means bulk upgrades to PHP v8.1 (or 8.0) should be completed before that time so that the rollback option is there available if needed.
  • Review PHP error logs. Most PHP compatibility issues are obvious as the website frontend or backend no longer works. Review PHP server logs for references to the lines of code causing issues. If the server logs are empty, try enabling WP_DEBUG and review the debug logs.

PHP’s implode function includes breaking changes.

PHP’s implode function requires the separator first then the array. In older versions of PHP, it used to work no matter which was first, the array or the separator string. I likewise had this same coding issue within my own toolkit that needed to be fixed. I suspect this one will be one of the most common coding mistakes found when upgraded to PHP v8.1.

Example of bad code: $classes = implode( $class_array,' ' );
Example of corrected code: $classes = implode( ' ', $class_array );

Common PHP 8 fatal errors and how to solve them.

Many PHP syntax errors which ran fine for PHP 7 will likely no longer work with PHP 8. PHP’s official “Backward Incompatible Changes” for PHP 8 is an excellent place to review all of the changes in the PHP language. Here are common errors I’ve seen while upgrading WordPress sites to PHP 8.1.

Example #1: Nested ternary operators

This is one of the first items mentioned on the PHP 7.4 deprecated features. Since ternary logic statements can be endlessly nested, correcting them can be tricky. Starting with PHP v8, nested ternary operators need to be wrapped with parentheses ( ).

PHP Fatal error: Unparenthesized a ? b : c ? d : e is not supported. Use either (a ? b : c) ? d : e or a ? b : (c ? d : e) in

Outdated code: $tag_html = empty( $template[ 'Content' ] ) ? '' : $template[ 'Content' ][ 'tag_html' ] ? trim( $template[ 'Content' ][ 'tag_html' ] ) : 'div';
Corrected code: $tag_html = ( empty( $template[ 'Content' ] ) ? '' : $template[ 'Content' ][ 'tag_html' ] ) ? trim( $template[ 'Content' ][ 'tag_html' ] ) : 'div';

Example #2: Failure to wrap string with quotes. This will result in undefined constants errors like: PHP Fatal error: Uncaught Error: Undefined constant "full" in ...

Outdated code: echo wp_get_attachment_image( $image, full );
Corrected code: echo wp_get_attachment_image( $image, "full" );

Example #3: Really old curly braces syntax

Support for deprecated curly braces for offset access has been removed. This format is typically only found on really old projects. Errors look like PHP Fatal error: Array and string offset access syntax with curly braces is no longer supported in ....

Outdated code: $array{0}; and $array{"key"};
Corrected code: $array[0]; and $array["key"];

Example #4: Missing arguments in functions that are extending a class.

PHP Fatal error: Declaration of thispart_sidebar_walker::walk($items, $depth) must be compatible with Walker::walk($elements, $max_depth, …$args) in ...

Outdated code: function walk($items, $depth) {
Corrected code: function walk($items, $depth, …$args) {

Example #5: Missing opening and closing parentheses ( ). This PHP fatal is going to vary depending on your situation as it’s technically just a PHP syntax issue. For my example the error given was PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, bool given. This is not very helpful as that is another common scenario as shown in the next example however not really an issue for this one.

Outdated code: if ( count( $meta_box['pages'] === 1 ) )
Corrected code: if ( count( $meta_box['pages'] ) === 1 )

Example #6: Attempting to use count on a noncountable variable.

Error message: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in ...

The solution will depend on its content. For a situation where you’re assigning the count to another variable, we can use ternary operators as shown below.

Outdated code: $unsub_count = count($unsub_reasons);
Corrected code: $unsub_count = is_countable($unsub_reasons) ? count($unsub_reasons) : 0;

For a situation where it’s part of a logic statement, we can add another check before running the count.

Outdated code: if ( count($unsub_reasons) > 0 ) {
Corrected code: if ( is_countable($unsub_reasons) && count($unsub_reasons) > 0 ) {

Example #7 Creating a function with create_function is no longer valid.

PHP Fatal error: Uncaught Error: Call to undefined function create_function() in ..

The solution is to rewrite it as an unnamed PHP function. This is described in more details with examples on PHP’s official docs: https://www.php.net/manual/en/function.create-function.php.

Outdated code: add_filter('gallery_style', create_function('$a', 'return "
";'));

Corrected code: add_filter('gallery_style', function( $a ) { return "
"; });

Example #8: Outdated widgets

Fatal error: Uncaught ArgumentCountError: Too few arguments to function WP_Widget::__construct(), 0 passed in /wp-includes/class-wp-widget.php:162

This one can be tricky to find the sections of code that need to be fixed. Sometimes reviewing the full stack tracks in the error logs can point you in the correct place. For this example, line #3 points to line 380 of functions.php.

Stack trace:
#0 /www/public/wp-includes/class-wp-widget-factory.php(61): WP_Widget->__construct()
#1 /www/public/wp-includes/widgets.php(115): WP_Widget_Factory->register()
#2 /www/public/wp-content/themes/custom-theme-international/functions.php(380): register_widget()
#3 /www/public/wp-includes/class-wp-hook.php(307): WP_CLI\Runner->{closure}()
#4 /www/public/wp-includes/class-wp-hook.php(331): WP_Hook->apply_filters()

That line will likely have a register_widget that will reference another class like this:

add_action( 'widgets_init', function() { return register_widget("YearlyArchiveWidget"); } );

Outdated code: function YearlyArchiveWidget() {

class YearlyArchiveWidget extends WP_Widget {
  function YearlyArchiveWidget() {
    $widget_ops = array('classname' => 'YearlyArchiveWidget', 'description' => 'A yearly archive of your site’s posts' );
    parent::__construct('YearlyArchiveWidget', 'Yearly Archive', $widget_ops);
  }

Corrected code: function __construct() {

class YearlyArchiveWidget extends WP_Widget {
  function __construct() {
    $widget_ops = array('classname' => 'YearlyArchiveWidget', 'description' => 'A yearly archive of your site’s posts' );
    parent::__construct('YearlyArchiveWidget', 'Yearly Archive', $widget_ops);
  }

Another way you can hunt for outdated code causing this error would be to search for all references to register_widget. Over the common line we can use grep.

grep -rn "register_widget" wp-content/

This should give you a list of class names which you can then search for in order to find and correct as needed.

Abandoned plugins are not compatible.

Sometimes plugins have no known solution and are unlikely to ever have a fix. In these situations, you can either attempt to patch the plugins yourself for PHP v8 or just look for an alternative. Here is a handful of a few that I found crashed customer websites.

Noising plugins.

Some plugins have compatibility issues that do not hinder functionality, just annoying background noise. The most widely used plugin I’ve seen with this issue is Facebook’s official plugin. As far as I can tell this PHP 8 bug has been around since August 2021.

Many folks have submitted fixes however Facebook hasn’t responded nor accepted those fixes. You can read more on that here. At this point, I’d recommend not using Facebook’s official plugin and just manually embedding their tracking code using WPCode.