# Two real WordPress plugins on PHP 8.4: what broke, and what I refused to auto-fix

> Source: <https://dev.to/zengbo/two-real-wordpress-plugins-on-php-84-what-broke-and-what-i-refused-to-auto-fix-bd4>
> Published: 2026-11-01 01:47:21+00:00

PHP 8.2 stops getting security fixes on December 31, 2026. PHP 8.3 already left active support at the end of 2025. If you maintain a WordPress plugin, a theme, or an older PHP app, the hosting company is going to move you to 8.3 or 8.4 sooner or later, whether the code is ready or not.

The hard part of that move is rarely "does it compile". It is three questions at once:

That third question is the one most tools skip, and it is why I built [Refit](https://github.com/qiyuanlabs-ai/refit), a small open-source CLI with two commands: `refit scan` and `refit fix`.

Rector is excellent at rewriting PHP safely, and PHPCompatibility knows every removed and deprecated PHP feature. Refit runs both, plus `php -l`. It does not reinvent the checks.

The catch is Rector's PHP version sets. They don't only remove breakages, they also modernize: `match`, `readonly`, `str_contains`. That is great for an app that only runs on PHP 8. It is a disaster for a plugin whose header says `Requires PHP: 7.2`, because the "upgraded" code now fails on the PHP version your users are still on.

So Refit does three things differently:

`Requires PHP`), `readme.txt` or `composer.json`.
Here is what `refit scan` found in a WordPress events plugin with 50 PHP files, targeting PHP 8.4. These numbers come straight from its Markdown report:

|  | Your code | Bundled libraries | 
|---|---|---|
| Breaks | 0 | 0 | 
| Check | 17 | 0 | 
| Deprecated | 5 | 0 | 
| Fixed by `refit fix` | 4 | not changed | 

Zero fatal errors, which is good news. But 17 spots can fail with unexpected input, and all of them are the same thing.

Since PHP 8.3, `DateTime::modify()` throws a `DateMalformedStringException` when it can't parse the string. Before, it returned `false` with a warning. So this very common pattern, taken from Refit's test fixture:

``` php
function legacy_next_date($when)
{
    $date = new DateTime();
    if (false === $date->modify($when)) {
        return null;
    }
    return $date;
}
```

still compiles, uses no removed function, and now dies with an uncaught exception the first time `$when` is an empty string from a URL or a saved setting.

Nothing in that code is "removed" or "deprecated", so a check for removed features has nothing to report. Refit ships its own PHP_CodeSniffer sniff for it. To keep the noise down, it only reports `->modify()` calls that:

`'+1 day'` either always works or always fails in testing),`try` block.
This version from the same fixture is not reported:

```
function legacy_safe_date($when)
{
    try {
        return (new DateTimeImmutable())->modify($when);
    } catch (Exception $e) {
        return null;
    }
}
```

These are reported as **Check**, not **Breaks**, because whether they fail depends on the data. A person has to decide what the right fallback is, so Refit doesn't try to write one.

The 4 auto-fixable items in the events plugin are dynamic properties, deprecated in PHP 8.2. Refit declares them in the class as plain public properties. That works when the parent class is in your code. When it isn't, as with this class from Refit's test fixture:

```
class Legacy_Widget extends WP_Widget
{
    public function __construct()
    {
        $this->widget_title = 'Legacy';
    }
}
```

Refit can't see `WP_Widget`, so it can't know whether declaring the property would clash with it. It adds `#[\AllowDynamicProperties]` instead. It does the same when the class or one of its parents defines `__get`, `__isset` or `__unset`, because a declared property would stop those magic methods from running for that name. Rector's own rule for this (`CompleteDynamicPropertiesRector`) is deprecated, so this is one of Refit's two own rules.

The report also has a section called "Already a problem on PHP 7.2". For the events plugin it holds 6 findings: `mysql_*` calls in the debug page and `$this` used in a plain function. These already fail on the oldest PHP version the plugin supports, so the upgrade doesn't change them. Keeping them apart means the headline answers the question you actually asked: what does moving to 8.4 break?

A social media feed plugin (46 PHP files) came back almost clean:

Runs on PHP 8.4, with 1 deprecation warning that will fill the error log and break in a later PHP version.

The one problem is an implicitly nullable parameter, deprecated in PHP 8.4:

```
Implicitly marking a parameter as nullable is deprecated since PHP 8.4. Update
the type to be explicitly nullable instead. Found implicitly nullable parameter:
$next_page_token.
```

The fix is trivial: write `?Type $x = null`. Rector has a rule for it. But the report says "Fixed by `refit fix`: 0".

That's on purpose. The plugin declares PHP 7.0 as its minimum, and the `?Type` syntax needs PHP 7.1. Writing it would turn a deprecation notice on PHP 8.4 into a parse error on PHP 7.0. So Refit reports the issue, explains the fix, and leaves the decision to the maintainer: raise the minimum to 7.1, or live with the notice for now.

`refit fix` applies the safe fixes in place. It's deliberately careful:

`git diff` and undo. (`--force` skips this.)`php -l` on every changed file before and after. A file that compiled before and doesn't after is restored to the original.`vendor/`, Freemius, TCPDF, Action Scheduler, CMB2 and others) are left alone unless you pass `--include-libraries`. The fix there is to update the library.
The test suite runs `scan` and `fix` end to end on a small sample plugin. Part of it looks like this before the fix:

``` php
public function save(array $data = null)
{
    $first = $data{0};
    return implode($data, ',');
}
```

After `refit fix`, the test checks that `$data{0}` became `$data[0]`, that `implode($data, ',')` became `implode(',', $data)`, that the parameter became `?array $data = null` (the fixture declares PHP 7.2, so `?Type` is allowed), that `create_function()` and `each()` are gone, that the bundled library file is byte-for-byte unchanged, and that every changed file still passes `php -l`.

This is the part I found most interesting. I started with Rector's per-version rules and ran them against real WordPress plugins. These five changed behavior, so Refit doesn't use them:

`define()` for. That breaks mixed-case constants defined in another file.`$x = ''` into `$x = []` even when `$x` is later used as a string. It broke a SQL builder.`func_get_args()`.`(string)` casts that PHP 8 already implies.
None of these are bugs in Rector. They're correct for the code Rector expects. Plugin code that has been patched across ten PHP versions just isn't that code.

Refit is static analysis. It never runs your code or loads your autoloader, so it can't see problems that depend on data, like passing `null` to a string function on PHP 8.1. There's an opt-in `--all-rules` flag that casts those arguments everywhere, but it's noisy, so it's off by default.

It also only scans `.php`, `.inc` and `.phtml` files, so Drupal's `.module` and `.install` files are not checked yet. And PHPCompatibility's PHP 8 checks only exist in its 10.0 alpha, which is why the install command has to name it.

Run your tests and click through the plugin on the target PHP version after fixing. The report says so too.

```
composer require --dev qiyuanlabs/refit "phpcompatibility/php-compatibility:^10.0@alpha"

vendor/bin/refit scan --php=8.4 --html=report.html
```

`--markdown` and `--json` work the same way. In CI, `refit scan --php=8.4 --fail-on=breaks` exits with code 1 when your own code has anything that breaks on the target version. It needs PHP 8.1 or newer to run, and the code you scan can target anything from PHP 5.3 up.

It's MIT licensed and at version 0.1.1, so I'd really like to hear where it's wrong: false positives from the DateTime check, rules you think are missing, or plugins where a fix changed behavior. Issues are open on [GitHub](https://github.com/qiyuanlabs-ai/refit).

I've been writing PHP for 18 years, and these days I also do fixed-price PHP upgrades at [QiYuan Labs](https://php.qiyuanlabs.ai) for the projects that need more than a tool.
