<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alban Bajraktari</title>
    <description>The latest articles on DEV Community by Alban Bajraktari (@instantprogramming).</description>
    <link>https://dev.to/instantprogramming</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4099341%2F93f6ff1e-d574-4d54-9b8b-3230f20c5bec.png</url>
      <title>DEV Community: Alban Bajraktari</title>
      <link>https://dev.to/instantprogramming</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/instantprogramming"/>
    <language>en</language>
    <item>
      <title>I ran a PHP 8.4 compatibility scanner against the 20 most-installed WordPress plugins. It was wrong 6 times, and right about Jetpack.</title>
      <dc:creator>Alban Bajraktari</dc:creator>
      <pubDate>Fri, 28 Aug 2026 18:02:55 +0000</pubDate>
      <link>https://dev.to/instantprogramming/i-ran-a-php-84-compatibility-scanner-against-the-20-most-installed-wordpress-plugins-it-was-wrong-1pp4</link>
      <guid>https://dev.to/instantprogramming/i-ran-a-php-84-compatibility-scanner-against-the-20-most-installed-wordpress-plugins-it-was-wrong-1pp4</guid>
      <description>&lt;p&gt;I've been building a static analyzer that finds PHP 8.x incompatibilities in WordPress plugins and themes. Full disclosure up front: it's my tool, there's a link at the bottom, and this post is the validation run — not a demo.&lt;/p&gt;

&lt;p&gt;The problem with writing a compatibility scanner is that it's trivially easy to write one that looks impressive. Flag every &lt;code&gt;each()&lt;/code&gt;, every &lt;code&gt;create_function()&lt;/code&gt;, every dynamic property, and you'll produce a long, frightening report for any codebase. The report will also be mostly wrong, and the first client who acts on it will break their site.&lt;/p&gt;

&lt;p&gt;So the question I actually needed to answer was: &lt;strong&gt;how often does it cry wolf?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The test rig
&lt;/h2&gt;

&lt;p&gt;I took the 20 most-installed plugins on wordpress.org, at their current versions, and scanned all of them. 16,663 PHP files.&lt;/p&gt;

&lt;p&gt;The logic is simple. These plugins are maintained, widely deployed, and run on PHP 8.4 across millions of sites right now. If my scanner reports a &lt;em&gt;certain fatal error&lt;/em&gt; in an up-to-date copy of WooCommerce, the overwhelmingly likely explanation is that my scanner is broken — not that WooCommerce is. That inverts the usual problem: instead of hunting for bugs I can't verify, every finding becomes a claim I'm obligated to disprove.&lt;/p&gt;

&lt;p&gt;The gate I set before running anything:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Zero false "certain fatal" and zero false "silent failure" on up-to-date plugins. Every deprecation reported must point at a real line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  First run: 99 fatals
&lt;/h2&gt;

&lt;p&gt;The first pass reported &lt;strong&gt;99 &lt;code&gt;fatal&lt;/code&gt; findings and 212 &lt;code&gt;deprecation&lt;/code&gt; findings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;99 certain fatals across twenty plugins that demonstrably work is, of course, nonsense. So I read all 99 in context — the surrounding ±40 lines, plus a search for guards elsewhere in the file and in the package — along with a 57-finding sample of the deprecations. 156 hand-written verdicts.&lt;/p&gt;

&lt;p&gt;They came out like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GUARDED&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;Real legacy code, deliberately unreachable on PHP 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TRUE POSITIVE&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;Real, correctly reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FALSE POSITIVE&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;My scanner was simply wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 103 are the interesting number. They aren't bugs in my scanner's pattern matching — the patterns matched real &lt;code&gt;mysql_*&lt;/code&gt; calls, real &lt;code&gt;mcrypt&lt;/code&gt; usage, real removed functions. They're bugs in my scanner's &lt;em&gt;judgment&lt;/em&gt;. Mature plugins are full of code that cannot run on PHP 8, sitting behind a check that guarantees it never will:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;function_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'mysql_connect'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// driver kept for PHP 5 hosts, inert since PHP 7&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scanner that reports this as a fatal error is technically describing the code accurately and practically useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four things I had wrong
&lt;/h2&gt;

&lt;p&gt;Each of these became a fixture — one file that breaks and one that doesn't — executed under PHP 7.4 and PHP 8.4 to prove the rule does what I claim before it goes back in the engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Severity was a lie.&lt;/strong&gt; I had one bucket for "uses a removed symbol." But a removed function called at the top level of a file is a guaranteed crash on load, whereas the same call inside a method body only crashes if something calls that method. Those are different products for the person reading the report: one is "your site is down", the other is "your site may go down." I split off a &lt;code&gt;conditional&lt;/code&gt; severity. Only top-level code and genuine compile errors kept the "certain fatal" label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Guards were invisible.&lt;/strong&gt; Adding guard detection — &lt;code&gt;function_exists&lt;/code&gt;, &lt;code&gt;extension_loaded&lt;/code&gt;, &lt;code&gt;defined&lt;/code&gt; in the same file — moved 103 findings down to informational. This one change is most of the difference between a useful report and a scary one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Nested ternaries broke on templates.&lt;/strong&gt; PHP 8 made unparenthesized nested ternaries a compile error, so detecting them matters. My detector counted ternary depth across &lt;code&gt;?&amp;gt; ... &amp;lt;?php&lt;/code&gt; boundaries, which meant an ordinary view file with two separate ternaries in two separate PHP blocks looked like one illegal nested ternary. That produced two fake fatals in Wordfence's view files. Depth now resets at block boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Dynamic properties over-fired.&lt;/strong&gt; PHP 8.2 deprecated writing to undeclared properties, and a naive detector flags a lot of code that's fine. Two exclusions fixed it: abstract classes, where the subclass declares the property (phpseclib, UpdraftPlus), and closures, where &lt;code&gt;Closure::call()&lt;/code&gt; rebinds &lt;code&gt;$this&lt;/code&gt; to an object that does declare it (WooCommerce's legacy data handler).&lt;/p&gt;

&lt;h2&gt;
  
  
  Second run
&lt;/h2&gt;

&lt;p&gt;After the fixes, across all twenty plugins: &lt;strong&gt;&lt;code&gt;fatal = 0&lt;/code&gt;, &lt;code&gt;silent = 0&lt;/code&gt;.&lt;/strong&gt; The gate passed.&lt;/p&gt;

&lt;p&gt;What remained: 43 &lt;code&gt;conditional&lt;/code&gt;, 208 &lt;code&gt;deprecation&lt;/code&gt;, 54 &lt;code&gt;info&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't expect: Jetpack
&lt;/h2&gt;

&lt;p&gt;Of those 43 conditionals, 41 traced back to deliberate legacy paths — an all-in-one-wp-migration factory guarded by &lt;code&gt;PHP_MAJOR_VERSION &amp;gt;= 7&lt;/code&gt; in a different file, mysql and mcrypt drivers in UpdraftPlus and Wordfence that PHP 8 can't reach.&lt;/p&gt;

&lt;p&gt;Two didn't. Both in Jetpack 16.1.2, both in &lt;code&gt;jetpack_vendor/automattic/jetpack-waf/src/class-brute-force-protection.php&lt;/code&gt;, at lines 678 and 777.&lt;/p&gt;

&lt;p&gt;The shape is this: a deprecated wrapper declared &lt;code&gt;static&lt;/code&gt; calls, via &lt;code&gt;self::&lt;/code&gt;, a method that is not declared static. Reduced to its essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Shape&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;deprecated_wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;real_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// self:: to a non-static method&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;real_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'ok:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under PHP 7.4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deprecated: Non-static method Shape::real_method() should not be called
statically in /app/jetpack-shape.php on line 9
ok:1.2.3.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under PHP 8.4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Non-static method Shape::real_method() cannot be called statically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns the right answer on 7.4 and throws on 8.4. In Jetpack the two affected wrappers are &lt;code&gt;ip_is_whitelisted()&lt;/code&gt; and &lt;code&gt;is_current_ip_whitelisted()&lt;/code&gt;, both deprecated in favour of &lt;code&gt;ip_is_allowed()&lt;/code&gt; / &lt;code&gt;is_current_ip_allowed()&lt;/code&gt;, and both with no guard of any kind.&lt;/p&gt;

&lt;p&gt;I reported it upstream before publishing this post: &lt;a href="https://github.com/Automattic/jetpack/issues/51728" rel="noopener noreferrer"&gt;Automattic/jetpack#51728&lt;/a&gt;. It's not a security issue — the code throws, it doesn't quietly let anything through — so the public tracker is the right place for it.&lt;/p&gt;

&lt;p&gt;I want to be precise about what this is and isn't. These are deprecated wrappers. A site only hits the error if something still calls them — old code, another plugin, a theme that copied a snippet from a 2021 blog post. That's exactly why the finding is graded &lt;code&gt;conditional&lt;/code&gt; and not &lt;code&gt;certain fatal&lt;/code&gt;, and it's why nobody has noticed. It's also a real defect in the current release of a plugin installed on millions of sites, found by a scanner whose entire purpose in this exercise was to be proven wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Compatible" and "quiet" are not the same thing
&lt;/h2&gt;

&lt;p&gt;The other thing the corpus made obvious: all twenty plugins are PHP 8.4 compatible in the sense that matters — they don't crash. All twenty are not therefore silent.&lt;/p&gt;

&lt;p&gt;208 verified deprecations, and every single one I sampled pointed at a real line. Implicit nullable parameter types, deprecated in 8.4, throughout vendored libraries — Symfony components, Guzzle 3, php-jwt, sodium_compat, phpseclib. &lt;code&gt;utf8_encode()&lt;/code&gt; in WooCommerce, deprecated in 8.2. Dynamic properties and out-of-order parameter defaults in UpdraftPlus, which alone accounts for 160 of them.&lt;/p&gt;

&lt;p&gt;None of that takes a site down. All of it lands in &lt;code&gt;debug.log&lt;/code&gt;, and on a busy site a log that fills with hundreds of notices per request is how you fail to notice the one notice that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits I'm not going to pretend aren't there
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Guard detection looks in the same file. A guard at the instantiation site in a different file leaves the finding at &lt;code&gt;conditional&lt;/code&gt; — honest, but it means a human still has to triage.&lt;/li&gt;
&lt;li&gt;Function names passed as strings (callbacks) aren't resolved.&lt;/li&gt;
&lt;li&gt;Null-to-internal-function detection only sees literal nulls. I chose precision over recall.&lt;/li&gt;
&lt;li&gt;Files that won't parse under either grammar fall back to lexical analysis and get a &lt;code&gt;parse-error&lt;/code&gt; finding. None occurred in this corpus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scan is read-only and purely static — scanned code is never executed. The only PHP that ran during any of this was my own fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I built this as a free WordPress plugin: it scans &lt;code&gt;wp-content&lt;/code&gt; for PHP 8.0→8.4 incompatibilities, sorts them into fatal / silent failure / cosmetic, and produces a report a non-developer can read. It proposes fixes as diffs and never applies anything on its own. It's here: &lt;a href="https://wprescue.instant-programming.com/?utm_source=devto&amp;amp;utm_content=etude-top20" rel="noopener noreferrer"&gt;https://wprescue.instant-programming.com/?utm_source=devto&amp;amp;utm_content=etude-top20&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you maintain a plugin in the top 20 and you think one of these verdicts is wrong, I'd genuinely like to hear it — every finding above is traceable to a file and a line, and I'd rather correct the record than defend a number.&lt;/p&gt;

</description>
      <category>php</category>
      <category>wordpress</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
