Rector is a PHP code transformation tool that applies AST (Abstract Syntax Tree) rewrites to source code using configurable rule sets — where PHPStan finds type errors and reports them, Rector fixes them by rewriting the code. It operates on PHP files using the nikic/php-parser AST library, applies one or more transformation rules to each node in the syntax tree, and writes the modified files back to disk. The transformation output is deterministic — the same input always produces the same output — making it safe to run in CI pipelines and commit the results as reviewed pull requests. Rector ships with hundreds of built-in rules organized into sets: LevelSetList::UP_TO_PHP_81 applies all transformations to make code valid PHP 8.1 syntax (typed properties, readonly, enum, never return type, intersection types, fibers); SetList::CODE_QUALITY simplifies redundant ternaries, removes dead assignments, and replaces strpos() !== false with str_contains(); SetList::TYPE_DECLARATION infers property types from constructor assignments and adds void return types to methods that have no return statement. For WordPress plugin development, Rector’s most practical use cases are: (1) modernising legacy plugins still using PHP 5.6–era patterns (array syntax, string functions, ereg_* calls) for PHP 8.0+ compatibility; (2) adding typed properties and return types to plugin classes so PHPStan can analyse them at level 6+; (3) batch-renaming hook callbacks after a plugin API refactor. The rector.php configuration file at the project root specifies paths to process, PHP version targets, rule sets, individual rules, and skip patterns — it is committed to version control so every developer and CI run uses the same configuration. Rector’s --dry-run flag prints a coloured diff of all changes without writing files, enabling safe review before applying. The PHPStan static analysis post covered finding type errors; Rector closes the loop by automatically fixing the errors PHPStan reports.
Problem: A WordPress plugin written in 2017 uses PHP 5.6 syntax throughout: untyped properties, missing return types, array() syntax, strpos() !== false conditionals, and bare is_null() calls. PHPStan at level 5 reports 230 errors. Manually fixing all of them is estimated at 3–4 hours of mechanical changes with high risk of introducing typos.
Solution: Configure Rector with PHP 8.1 level sets and code-quality rules, run a --dry-run to review the diff, apply the changes, then re-run PHPStan to confirm the error count drops from 230 to the residual business-logic errors Rector cannot infer automatically.
// rector.php — configuration at the project root
// Install: composer require rector/rector --dev
// Run: vendor/bin/rector process
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
return RectorConfig::configure()
// Paths Rector will scan (relative to project root)
->withPaths([
__DIR__ . '/wp-content/plugins/my-plugin/src',
__DIR__ . '/wp-content/themes/my-theme/inc',
])
// Bootstrap WordPress so Rector can resolve WP function signatures
->withBootstrapFiles([
__DIR__ . '/wp-load.php',
])
// Upgrade to PHP 8.1 language features
->withPhpSets(php81: true)
// Individual rulesets (add only what you intend to apply)
->withSets([
SetList::CODE_QUALITY, // simplify ternaries, remove dead code, etc.
SetList::TYPE_DECLARATION, // add typed properties and return types
])
// Individual rules
->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
TypedPropertyFromAssignsRector::class,
])
// Skip specific files, directories, or rule+path combinations
->withSkip([
// Skip third-party vendor code
__DIR__ . '/wp-content/plugins/my-plugin/vendor',
// Skip a specific rule in generated/legacy files
NullToStrictStringFuncCallArgRector::class => [
__DIR__ . '/wp-content/plugins/my-plugin/src/Legacy',
],
])
// Rector will process files but NOT write changes — use for dry-run review
// ->withDryRun()
// Show diff of each change in the output
->withImportNames(removeUnusedImports: true);
# Install Rector as a dev dependency
composer require rector/rector --dev
# Dry-run: preview all changes without writing files
vendor/bin/rector process --dry-run
# Apply all changes
vendor/bin/rector process
# Process only a single file during development
vendor/bin/rector process wp-content/plugins/my-plugin/src/MyClass.php
# Show which rules triggered changes (verbose)
vendor/bin/rector process --debug
# CI pipeline: fail if Rector would make changes (code must be already modernised)
vendor/bin/rector process --dry-run --no-progress-bar
# exit code 1 if changes needed, 0 if clean
# After applying Rector changes, re-run PHPStan to check remaining errors
vendor/bin/phpstan analyse wp-content/plugins/my-plugin/src --level=6
// ── Example: before and after Rector transforms ───────────────────────────────
// BEFORE (PHP 5.6 style)
class My_Widget extends WP_Widget {
private $cache_key;
private $timeout;
public function __construct() {
$this->cache_key = 'my_widget';
$this->timeout = 3600;
parent::__construct( 'my_widget', 'My Widget', array() );
}
public function widget( $args, $instance ) {
$title = apply_filters( 'widget_title', $instance['title'] );
if ( strpos( $title, 'Hello' ) !== false ) {
echo $args['before_widget'];
}
if ( is_null( $instance['count'] ) ) {
return;
}
}
}
// AFTER (Rector applied php81, code_quality, type_declaration sets)
class My_Widget extends WP_Widget {
private string $cache_key; // typed property inferred from assignment
private int $timeout;
public function __construct() {
$this->cache_key = 'my_widget';
$this->timeout = 3600;
parent::__construct( 'my_widget', 'My Widget', [] ); // array() -> []
}
public function widget( $args, $instance ): void { // void return type added
$title = apply_filters( 'widget_title', $instance['title'] );
if ( str_contains( $title, 'Hello' ) ) { // strpos !== false -> str_contains
echo $args['before_widget'];
}
if ( $instance['count'] === null ) { // is_null() -> === null
return;
}
}
}
NOTE: Rector rewrites files in-place — always commit your code to Git before running Rector so you can review the diff with git diff and revert individual changes with git checkout if a transformation is incorrect. Some transformations require human review: Rector may infer a property type as string from an assignment in the constructor, but if another method later assigns null to it, the inferred type should be string|null — PHPStan at level 6 will catch this after Rector runs. The recommended workflow is: run Rector → run PHPStan → fix remaining PHPStan errors manually → commit. Rector is also available as a GitHub Action (rectorphp/rector-action) that can open a pull request with all automated upgrades applied, enabling code-review of modernisation changes through the normal PR process.