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.)

Ways the WordPress website shoots itself in the foot

Even though I genuinely love WordPress, and have literally built the last decade of my career around it, there are plenty of things that bother me.

Two things that really bother me: page builder plugins/themes, and mediocre hosting providers.

Why page builder plugins/themes? Because they do not do things “the WordPress way.” Yes, they were originally designed to address woeful limitations in the default WordPress editing interface, but 1) they are typically so loaded up with their own interface conventions that they don’t “feel” like WordPress at all, 2) they’re often so convoluted that they don’t really make WordPress easier to use, they just make it easier for people who don’t know how to code to still get paid to build websites, and 3) they are rapidly being made redundant by the built-in Block Editor (Gutenberg) that has now been part of WordPress core for four years, and is actually starting to get pretty good.

Why mediocre hosting providers? Because they’re mediocre. But seriously, “low-rent” commodity hosting providers are the absolute worst way to host a WordPress website. Their tools and performance are inadequate. If you know sysadmin stuff, you’re better off with a VPN from the likes of Linode. And if you don’t, then you really should spend a bit extra for a premium, WordPress-optimized managed hosting service like WP Engine.

Why then is it, when you go to the WordPress Themes page, the first non-stock theme you are presented with is, of all things, Hello Elementor, built around the dreadful Elementor page builder? Why are they promoting this? I’m not saying page builders shouldn’t exist. I’m just saying people shouldn’t use them. And I definitely do not think that WordPress should be heavily promoting one of them as a preferred way for new users to get a WordPress site up and running! Why still is it the first third-party theme on the page?

I do see Astra, a very well-designed, Block Editor-friendly third-party theme — one I have even used on a client site! — is the second third-party theme featured. But it absolutely deserves to come long before anyone even thinks of Elementor.

As for hosting, this is another place where I think WordPress is making a major stumble, mainly because this is a third-level page in the site hierarchy, but it is very prominently linked from a page for people who are just getting started and looking for a place to host their new (first!) WordPress site. But this page has not been updated in years, and its recommendations desperately need revisions.

I have worked with all three of the “recommended” providers — BlueHost, Dreamhost and SiteGround — only because clients came to me who were already using them. I emphatically do not recommend any of the three, although Dreamhost is… acceptable. But in this modern era when there are some really excellent, WordPress-focused, managed hosting companies like WP Engine, how is it that WordPress is still officially recommending these three dinosaurs of the early 2000s?

Artifacts of an error

A little over a week ago I did something really stupid. Today I took the final step in making amends for that error, which was ultimately the consequence of a questionable decision I made many years ago, to create an ad hoc WordPress multisite setup. That lowercase “m” is significant, and I hinted at it in the post I linked to above.

This was not a proper WordPress Multisite setup. I think that feature probably already existed at the time, although it may not have. This was my own concoction, created by adding a PHP switch to the wp-config.php file, telling it to use a different database and uploads directory depending on the domain name in the HTTP request. It allowed me to manage a number of my own and Sara’s websites with a single WordPress installation, but it was dangerous.

None of the sites “knew” about each other. That wasn’t really a problem, except when it came to managing themes and plugins. I always had to be careful to remember that just because my site wasn’t using a particular theme or plugin, didn’t mean one of Sara’s sites wasn’t… or even one of my own other sites in the setup.

I forgot about that little detail when I did my stupid thing a couple weeks ago. I deleted all of the themes my blog wasn’t using, forgetting that Sara was using many of them. In a panic to quickly restore the themes, I made matters worse by accidentally restoring the entire server from a 6-day-old backup, erasing all of the work Sara and I had done on our respective sites over the preceding week.

My “final step” in making amends was to at least remove my own sites from that setup, so it is now just running Sara’s sites. If you’re reading this, it means you’re seeing my blog in its new home. (Coincidentally, this is also facilitating my gradual transition away from Digital Ocean in favor of Linode for my hosting needs.) I also moved my John Coltrane and Adelia Haight McCormick sites, which were part of the same cockamamie arrangement.

One last thing that I didn’t think I’d be able to recover: I wrote two fairly substantial blog posts in that week, and I was pretty sure those were gone forever. But last night I discovered that the Feedly app on my iPhone still had them cached! I quickly snapped a series of screenshots so they wouldn’t be lost forever.

I had originally intended to retype them here, back dating them to their original post dates, to make it seem like they’d never been lost. But somehow it seems more fitting to just post those phone screenshots instead.

One of the posts hinged on a composite Mac screenshot I had taken from Safari, demonstrating its absurd Experimental Features menu, and that’s an image I still had on my Mac, so I’m including it below as well.


Also worth noting here… this is my first post on this blog using the Block Editor, a.k.a. Gutenberg. I’ve finally accepted its inevitability. I’ll probably update the site to a new theme in 2023 as well… this one is a custom hack of Twenty Eleven, for cryin’ out loud!

Why are major WordPress plugins not bothering to fix their numerous PHP 8.1 deprecation notices?

I’m an early adopter for a lot of things, but new versions of PHP are generally not one of them. PHP 8.1 wasn’t really on my radar until I set up a new server running Ubuntu Linux 22.04, where PHP 8.1 is the default version.

Yeah, I’m a commercial WordPress plugin developer too, but my business is small. It’s easy to understand me not being 100% on top of this.

What I do not understand is how huge WordPress plugins like Jetpack, WooCommerce and Wordfence can get away with not being on top of it.

The general response these developers are giving when questioned on this is, “they’re only deprecation notices, everything should still be working.” Which is true.


Are you a developer? Do you ever need to turn debugging on? Have you seen what happens when you have multiple plugins active, each of which generates 5-10 PHP deprecation notices on every page when debugging is turned on?

Aside from making the site, and especially the WordPress backend, borderline-to-entirely unusable (which, honestly, is a bigger problem than what I’m about to say), it also makes normal debugging impossible. It’s hard to find real issues when you have to wade through more than a screen’s worth of irrelevant deprecation notices. And the sheer volume of the notices breaks page layouts to a point where it’s impossible even to know what is or isn’t broken.

I actually blame PHP for this. I don’t know what they were thinking with some of these changes that are triggering all of the deprecation notices. I know “real” programming languages aren’t as loosely typed and lenient with sloppy coding as PHP is, but… well, it’s kind of too late to put that genie back in the bottle. I haven’t bothered to investigate exactly what the rationale is for no longer allowing null input to string handling functions. What I do know is that there’s a ton of code out there, written by competent, experienced developers, that is now triggering these warnings, because until now it was always fine to equate null and an empty string.

Anyway, principled arguments aside, I have a more practical frustration to deal with right now… I’m finding it hard to do my work, because other people haven’t done theirs.

Making Bootstrap work with the WordPress Block Editor

tl;dr: You need to load Bootstrap’s CSS in the 'wp_enqueue_scripts' hook with a priority lower than 10.

Where do I begin with this one? First, some foundational details: I’m old school. I’ve been a professional web developer/designer since 1996. I’ve embraced new technologies as they come along, but I tend to bristle at things that are either a) not an open standard or b) need to be compiled.

I refused to use Flash. In the end I was proved right, as open web technologies supplanted it. Flash is dead, and the open web is not. I still refuse to use CSS preprocessors for a similar reason. They’re a non-standard workaround to limitations in the current standard. They fragment the landscape instead of pushing the standard forward. And as CSS variables have gained wide browser support, preprocessors are beginning to look as pointless as Flash.

Now the thing I hate is npm — or any similar tech that requires me to run shell scripts and compile anything. I’ve long since embraced server-side scripting, using open source libraries like jQuery, and even using a pre-built CMS (or CMS-ish system) like WordPress. (Yes, for many years I rolled my own CMSes.) The bottom line for me is, if it’s code I can download simply and treat as a “black box,” fine. But if it’s something I need to get my hands dirty in, I shouldn’t need anything to work with it but a text editor.

All of that to say, I am having a bit of a time with where things are going these days with Gutenberg, a.k.a. the WordPress Block Editor. And I haven’t exactly been quiet about it here.

This week I needed to extend the Block Editor by creating a carousel/slideshow block. Yep, I’ve rolled my own with these for many years as well, but this time around I decided I wanted to work with something pre-built, so I settled on the one in Bootstrap. I don’t really need all of Bootstrap (although I suspect over time I will need more of it), but customizing it requires using npm, so I decided to go ahead and include the entire pre-compiled package in my theme.

That’s when the problems began.

Oh, the carousel works great! But I spent a bunch of time yesterday trying to figure out why the custom background color and fonts defined in my theme.json file were being ignored… especially since they’re right there in the inline CSS inside the <head> of every page.

Don’t even get me started on inline CSS, or the way so many people in the industry seem to worship PageSpeed Insights. Once again we’re skating to where the puck is, instead of where it’s going, and to stretch the analogy to the limit, we’re melting all of the ice in front of it too. This is not the way to move web development forward intelligently.


Eventually I worked out what’s happening. WordPress, historically, was designed to allow you to define dependencies, so you could load CSS and JavaScript in a logical manner. Gutenberg/Block Editor throws all of this out the window with this inline CSS. Certain “critical” inline CSS for the Block Editor gets loaded immediately regardless of the dependencies you put into wp_enqueue_style(). The result being, styles defined (indirectly, in a way more convoluted way than vanilla CSS) in my theme.json file were appearing before the Bootstrap CSS file was loaded. And since I’m using the compiled Bootstrap instead of a custom npm build, there’s a bunch of “general” CSS it’s throwing in, including color and font definitions on the <body> tag that override Gutenberg’s earlier inline CSS.

Fortunately, someone else had the same problem and figured out a solution. But since, in 2022, spammers overrun any forum thread that’s left open too long, the thread was already locked and I couldn’t express my appreciation to the poster who shared it. So I’m writing this blog post instead.

The trick is to load any third-party CSS that you might need to override using a separate function called on the 'wp_enqueue_scripts' hook, with a low priority number (less than 10, since that’s the magic default).

Here’s a generalized version of the code I’m using:

function my_enqueue_scripts_early() {

    $ver = '1.0.0'; // Your theme version; could also be a class property, constant, global variable, etc.

    wp_enqueue_style('bootstrap-style', get_theme_file_uri('vendors/bootstrap/bootstrap.min.css'), array(), $ver);
    wp_enqueue_script('bootstrap', get_theme_file_uri('vendors/bootstrap/bootstrap.bundle.min.js'), array('jquery'), $ver);

add_action('wp_enqueue_scripts', 'my_enqueue_scripts_early', 8);