Debugging WordPress and PHP 8.1: a chicken-and-egg conundrum

If you’re a WordPress developer, trying to debug code on a server that’s running PHP 8.1, you may have noticed an absurd number of deprecation notices overwhelming your efforts to get anything done.

After trying in vain to resolve the issue by updating the value for error_reporting in my server’s php.ini file, I discovered why that doesn’t work, courtesy of a StackExchange answer.

WordPress sets its own value for error_reporting when you turn on WP_DEBUG, ignoring the php.ini value. It kind of has to do this. (Well, not “kind of.”) That’s the only way for WordPress to display more — or less — error output than what’s configured at the server level.

The problem is, when you turn on WP_DEBUG, WordPress shows you everything. Normally that would be desirable, but PHP 8.1 has introduced an unusually large number of deprecation notices in anticipation of PHP 9 imposing strict rules on things that have been generously allowed in earlier PHP versions.

OK, so we know what’s going on. But since a lot of the deprecated code is in WordPress core, or in third-party plugins, there’s not really anything a developer like me can do about fixing these issues. (Sure, I could fix it and submit a pull request, but I’m not currently a WordPress core developer and I am not sure I want to take that on, even in an extremely peripheral way.)

So… uh… how do I just make it stop? That is definitely easier said than done, and the reason is the sequential nature of how code is loaded and executed. “Everything everywhere all at once” is not how it works. WordPress loads one file, that loads another file, that loads several other files, each containing a mix of procedural and object-oriented code, and functions. The way WordPress lets you hook into that flow and insert modifications is… well.. hooks.

Hooks are great, but a) the hook you want has to exist, and b) your code that uses the hook needs to be loaded before WordPress processes the hook. Oh, and of course, c) hooks themselves are functions, so you can’t use them until those functions have been defined.

Hence our problem. The code that tells WordPress to show you all the stuff — errors, warnings, deprecation notices — happens pretty early in the sequence. Specifically (as of WordPress 6.1.1) it is in line 460 of wp-includes/load.php in a function called wp_debug_mode(). By that point, yes, the add_action() and add_filter() functions have been defined. But, WordPress hasn’t actually loaded any plugins yet (even “must-use” plugins in the mu-plugins folder). So if you write a plugin to modify the error_reporting value, it might work, but only on deprecation notices that are generated after your plugin has been loaded, and the ones we’re concerned with are all in WordPress core and get triggered before plugin loading starts.

Realizing this, I thought I might solve the problem by putting my filter into the wp-config.php file, a.k.a. the only “early” file you’re allowed to edit. But nope, can’t do that: the add_filter() function doesn’t exist until wp-includes/plugin.php gets loaded at line 49 of wp-settings.php, which itself gets loaded at the very end of wp-config.php.

Since wp_debug_mode() runs at line 80 of wp-settings.php, that means the only way to do what we’re trying to accomplish is to get it to fire off somewhere within those 31 lines of code inside wp-settings.php. Those lines consist of calls to a handful of low-level functions. I checked the source code of each of them for any hooks — not that it would be correct to use the hooks in any of those functions for this purpose, if they existed — but merely to see if it would even be possible.

There is only one hook in the entire lot, and it’s inside wp_debug_mode() itself. It’s called enable_wp_debug_mode_checks. I wrote my own filter function that leverages that hook to modify error_reporting, and it would work, except for the fact that there’s nowhere to put it. I can’t write any custom code in a plugin or theme to call that filter, because it wouldn’t be loaded yet by the time the filter is applied in wp_debug_mode(). And I can’t put it in wp-config.php because, as noted above, the add_filter() function isn’t even defined yet at that point.

So… there are only two place you can put this code to get it to work: either in wp-settings.php just before line 80, or by just editing the wp_debug_mode() function itself in wp-includes/load.php. And you very much are not supposed to do either of these things, because your changes will get overwritten the next time a WordPress core update runs.

But… what else are you going to do? Well… after going through all of the emotions on my wide spectrum from frustration to rage, I read the comments at the top of wp_debug_mode() that start with a pretty unambiguous statement:

This filter runs before it can be used by plugins. It is designed for non-web runtimes.

OK then.

Also inside the comment is a code example, mirrored in the next reply on the same StackExchange post I linked above. I initially ignored it because I instinctively ignore any PHP code example that includes $GLOBALS… but in this case, it’s apparently the official answer on the matter. Boo.

The code I ended up putting into wp-config.php looks a bit different though:

if (WP_DEBUG) {
  $GLOBALS[‘wp_filter’] = [
    ‘enable_wp_debug_mode_checks’ => [
      10 => [[
        ‘accepted_args’ => 0,
        ‘function’ => function() {
          error_reporting(E_ALL & ~E_DEPRECATED);
          ini_set(‘display_errors’, ‘on’);
          return false;
        },
      ]],
    ],
  ];
}

I’m not sure why the StackExchange poster put the error_reporting() call outside the conditional. I also found I needed to specifically set ini_set('display_errors', 'on'); because returning false from this function causes the rest of wp_debug_mode() not to execute — which we want, but we need to make sure to replicate any of the rest of its functionality that we do need. I probably should add the bit that doesn’t output errors on REST/AJAX calls, but I’ll worry about that when it becomes an issue. I don’t use either of those very often. (Of course the WP admin itself uses AJAX all the time.)

The most peculiar WordPress problem yet

I was beyond dismayed tonight to load up my browser and discover that my website, for whatever reason, just wasn’t working. Firefox would just bring up a completely blank screen. Safari returned a strange error:

Safari can’t open the page “http://room34.com/”. The error was: “Operation could not be completed. (kCFErrorDomainCFNetwork error 302.)” (kCFErrorDomainCFNetwork:302) Please choose Report Bugs to Apple from the Safari menu, note the error number, and describe what you did before you saw this message.

Uhh… OK.

Needless to say, it’s not something I’ve ever seen before, and if Google is any indication, (almost) no one else has either.

After inspecting my files and my data tables to see if anything had been altered (i.e. hacked — and no, it hadn’t), and watching The Colbert Report, I decided I had no choice but to confront the rat king and start stepping through all of the nested include files that comprise the WordPress application, hoping to find the exact point at which the whole thing was going kerblooey.

Eventually I did manage to find the precise line in the precise file where the site ceased to return any output to the browser. It happens to be line 20 in wp-includes/taxonomy.php, which goes something like this (minus the insistent tendency of WordPress to automatically insert smart quotes everywhere):

$wp_taxonomies[‘link_category’] = (object) array(‘name’ => ‘link_category’, ‘object_type’ => ‘link’, ‘hierarchical’ => false);

Having abandoned efforts to further dissect the innards of WordPress, I tried commenting out the line. No luck. Next I tried just sticking a return; at the top of the file, essentially scuttling everything it does. That did display the page, but without the functions in this file it was so riddled with error messages as to be unusable. Next, I returned to line 20 and emptied out that array it creates (commented it out, to be precise), leaving, in essence:

$wp_taxonomies[‘link_category’] = (object) array();

And, whaddayaknow… it freakin’ works! In fact, on cursory perusal of the site, I don’t even see any problems, so I’m not sure where this particular subarray gets used, or whether my own idiosyncratic taxonomic structure even needs it. And I’m sure within approximately 24 seconds of posting this entry, I’ll discover it. But for now, things seem good… although I hate having to settle for a “solution” like this, especially given the effort it took to pinpoint the problem.

Update, May 30, 2008: I was informed by my hosting provider that last night they performed a PHP upgrade, which seems to have been what precipitated this problem. So perhaps I have discovered a conflict between PHP 5.2.6 and WordPress 2.5.1.

Update, June 2, 2008: After working briefly with my ISP on this issue, they have modified a configuration setting on the server to solve this problem. Here’s what they had to say about the change: The server was previously configured to support PHP4 backwards compatibility via the following directive:

zend.ze1_compatibility_mode 1

They’ve removed that compatibility mode setting in the configuration for my virtual host, as such:

zend.ze1_compatibility_mode 0

This change on their part has solved my problem, as I have now reinstated the original WordPress code in taxonomy.php and the site is functioning properly.