qiyuanlabs / refit
Check and fix PHP 8.x compatibility in WordPress plugins and plain PHP code, with a report your client can read.
Requires
- php: >=8.1
- phpcompatibility/php-compatibility: ^10.0@alpha
- rector/rector: ^2.8
- squizlabs/php_codesniffer: ^3.13.3 || ^4.0
- symfony/console: ^6.4 || ^7.0
- symfony/process: ^6.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-31 15:34:51 UTC
README
Find and fix what breaks when a PHP project moves to a newer PHP version, without modernizing code that still has to run on old PHP.
Refit is built for WordPress plugins and themes and for plain PHP projects that are not on Laravel or Symfony. It answers one question, "will this run on PHP 8.4, and what do I have to change?", and fixes the safe part for you.
$ refit scan wp-content/plugins/my-plugin
My Plugin · WordPress plugin 1.2.0 · 41 PHP files
Target PHP 8.4 · must keep running on PHP 7.2 (declared by the project)
Not ready for PHP 8.4: 7 problems will fail on PHP 8.4.
+--------------+-----------+-------------------+
| | Your code | Bundled libraries |
+--------------+-----------+-------------------+
| Breaks | 7 | 1 |
| Check | 1 | 0 |
| Deprecated | 7 | 1 |
| Auto-fixable | 11 | - |
+--------------+-----------+-------------------+
Why another tool
Refit does not reinvent checks. It runs three proven tools and adds the parts that are missing for a real upgrade:
| What it does well | What Refit adds | |
|---|---|---|
| PHPCompatibility | Knows every removed and deprecated PHP feature | Plain-language explanations, grouping, WordPress-aware noise filtering (WordPress polyfills many "new" functions) |
| Rector | Rewrites code safely | A curated list of compatibility-only rules. Rector's PHP sets also modernize code (match, readonly, str_contains), which breaks plugins that still support PHP 7. Refit only fixes breakages, and only with syntax your minimum PHP version understands |
php -l |
Tells you whether a file compiles | Runs on every file in parallel with the PHP you are upgrading to |
On top of that, Refit:
- Reads the plugin or theme header (
Requires PHP),readme.txtorcomposer.jsonto know the oldest PHP version the code must keep supporting. - Separates your own code from bundled libraries (Composer
vendor/, Freemius, TCPDF, Action Scheduler, CMB2 and others), because the fix there is to update the library. - Separates problems that are new on the target PHP from ones that already exist on your minimum PHP version.
- Declares dynamic properties (deprecated in PHP 8.2) as untyped public properties.
That is not behaviour-neutral:
property_exists(),get_object_vars()andjson_encode()include a declared property even before it is assigned, and__get,__issetand__unsetno longer run for that name. When the class or an ancestor defines one of those methods, or the parent class is not in the project (for exampleWP_Widget), Refit adds#[\AllowDynamicProperties]instead of declaring the property. Classes that define__setare left unchanged, because assignment already goes through that method. - Flags
DateTime::modify()calls with a non-literal argument and notry, which throwDateMalformedStringExceptionon PHP 8.3 instead of returningfalse. - Writes reports you can send to a client or attach to a ticket: HTML, Markdown and JSON.
Requirements
- PHP 8.1 or newer to run Refit. The project you scan can target anything from PHP 5.3 up.
- Git, if you want
refit fixto check that your work is committed first.
Install
With Composer, per project:
composer require --dev qiyuanlabs/refit "phpcompatibility/php-compatibility:^10.0@alpha"
Or globally:
composer global require qiyuanlabs/refit "phpcompatibility/php-compatibility:^10.0@alpha"
PHPCompatibility 10 is still tagged alpha, and it is the only release with checks for PHP 8.x. Composer does not let a package pull in an alpha dependency on its own, so it has to be named once in the install command.
A standalone PHAR download is planned.
Usage
Scan
refit scan [path] [options]
path defaults to the current directory. Nothing in the project is changed and none of
its code is executed.
| Option | Meaning |
|---|---|
--php=8.4 |
PHP version to upgrade to: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 or 8.5. Default 8.4 |
--min-php=7.2 |
Oldest PHP version the code must still run on. Default: what the project declares, else 7.2 |
--html=report.html |
Write a self-contained HTML report |
--markdown=report.md |
Write a Markdown report |
--json=report.json |
Write a JSON report. Use --json=- for standard output |
--fail-on=breaks |
Exit with code 1 when your own code has issues of this level or worse: breaks, review, deprecated or none (default) |
--exclude=dir |
Skip a directory name wherever it appears. Repeatable |
--include-tests |
Also check tests and test directories, which are skipped by default |
--all-rules |
Also count fixes that are correct but noisy, like casting possibly-null arguments for PHP 8.1 |
--php-binary=php8.4 |
PHP binary used for the compile check. Use the version you are upgrading to for the most accurate result |
--no-branding |
Leave the "Generated by Refit" line out of reports |
In CI:
refit scan --php=8.4 --fail-on=breaks
Fix
refit fix [path] [options]
Applies the automatic fixes in place, then prints what changed per rule. Review the result
with git diff and run refit scan again.
Refit is careful here:
- It refuses to run unless the project is a git repository with no uncommitted changes,
so you can always review and undo.
--forceskips this check. - It checks every changed file with
php -lbefore and after. A file that compiled before and does not compile after is restored to the original. - Bundled libraries are left alone unless you pass
--include-libraries. - Tabs or spaces are kept as the project uses them.
fix takes the same --php, --min-php, --exclude, --include-tests and
--all-rules options as scan.
Severity levels
- Breaks: a fatal error or removed feature on the target PHP version.
- Check: may fail at run time depending on the data, so a person should look.
- Deprecated: works, but fills the error log and will break in a later version.
What Refit fixes automatically
52 rules taken from Rector, covering PHP 5.5 to PHP 8.5, plus two of Refit's own. Among them: each(),
create_function(), curly brace offsets ($str{0}), PHP 4 constructors, reversed
implode() arguments, money_format(), "${var}" in strings, implicitly nullable
parameters (Type $x = null), and dynamic properties.
Each rule only runs when the problem actually exists on your target version, and only
when the code it writes still runs on your minimum version. For example, the fix for
implicitly nullable parameters writes ?Type, which needs PHP 7.1, so it is skipped for
a plugin that still declares PHP 7.0. The issue is still reported.
Rector rules left out on purpose
These were tested on real WordPress plugins and changed behavior, so Refit does not use them:
SensitiveConstantNameRectorupper-cases constants it cannot find adefine()for, which breaks mixed-case constants defined in another file.AssignArrayToStringRectorturns$x = ''into$x = []even when$xis later used as a string (it broke a SQL builder).RemoveExtraParametersRectordrops arguments that functions may read withfunc_get_args().WrapVariableVariableNameInCurlyBracesRectorrestores PHP 5 semantics in code that has been running on PHP 7 since.StringifyStrNeedlesRectoronly adds(string)casts that PHP 8 already implies.CompleteDynamicPropertiesRectoris deprecated in Rector. Refit has its own rule that handles classes extending WordPress core classes.
Limits
Refit is static analysis. It does not run your code, so it cannot see problems that only
appear with certain data, such as passing null to a string function on PHP 8.1 (use
--all-rules to cast those arguments everywhere) or a type error from a value read from
the database. Run your test suite and click through the plugin on the target PHP version
after fixing.
Code that is generated at build time (for example libraries copied in by a release script) is only checked if it is present in the folder you scan.
Safety
- Refit never executes project code. It never loads the project's autoloader: the compile
check uses
php -l, PHPCompatibility only tokenizes, and Rector uses static reflection. scanworks on a temporary copy and changes nothing.- No network access, no telemetry.
Development
composer install
vendor/bin/phpunit
The test suite includes an end-to-end run of scan and fix on the sample plugin in
tests/fixtures/legacy-plugin.
License
Refit is released under the MIT license. See LICENSE.
It depends on Rector (MIT), PHP_CodeSniffer (BSD-3-Clause), PHPCompatibility and PHPCSUtils (LGPL-3.0-or-later), and Symfony components (MIT), each under its own license.
Need help with a bigger upgrade?
Refit is made by QiYuan Labs, which does fixed-price PHP upgrades for WordPress plugins and PHP applications.