Using Rector to Automate PHP Version Upgrades
In 1890, the US Census faced a crisis. The 1880 census had taken eight years to complete—by the time it was done, the data was already stale. Processing millions of hand-written records with mechanical adding machines was simply too slow and error-prone. The solution came in the form of Herman Hollerith’s tabulation machine, which automated what had been a grueling manual process. The machine didn’t replace human judgment—it freed people from repetitive, error-prone work so they could focus on higher-value tasks.
You face a similar, if less monumental, challenge today. Your PHP application has evolved over years—perhaps a decade. It works, it serves its purpose, but it’s built on older patterns: deprecated functions, inconsistent styles, constructs that make you nervous to touch because they might break something subtle. Now a security requirement mandates PHP 8.1, but your codebase is still on 7.4. What do you do?
Historically, PHP version upgrades meant manual, painstaking work. Developers would comb through files hunting for mysql_* functions, replacing each() loops, updating constructor calls—all while crossing their fingers that nothing broke. For a medium application, this could take weeks. And human error? Practically guaranteed.
What if you could automate most of that work? Of course, manual upgrades are possible—Rector doesn’t eliminate the need for review and testing—but it frees you from the most tedious, error-prone tasks.
What is Rector?
Rector is a reconstructor tool that analyzes your PHP code and applies automated refactorings. Unlike simple search-and-replace tools, Rector understands the code’s structure—it uses an Abstract Syntax Tree (AST) to parse and transform PHP code safely. This means it doesn’t just find text patterns; it understands what the code does and can make intelligent changes that preserve behavior while modernizing syntax.
Strictly speaking, Rector is more than just an upgrade tool—it’s a general-purpose PHP refactoring engine with hundreds of pre-built rules. You can use it for everything from upgrading PHP versions to adopting new framework patterns, improving code quality, and enforcing coding standards. But for our purposes, we’ll focus on version migrations.
Why (and When) to Use Rector
Let’s be clear, though: Rector isn’t magic. It won’t fix every problem, and it won’t understand your business logic. But for the mechanical aspects of version upgrades—syntax changes, deprecated function replacements, and pattern migrations—it’s remarkably effective.
Here’s what Rector typically handles well:
- Consistent Syntax Updates: Converting old constructor syntax to property promotion, replacing
create_function()with closures, updating ternary operator associativity. - Deprecated API Replacements: Finding uses of
ereg()and replacing withpreg_match(), updatingmcrypttoopenssl, convertingsplit()toexplode()orpreg_split(). - Type System Improvements: Adding scalar type hints, return types, and converting docblocks to native types where possible.
- Framework-Specific Patterns: Modernizing Symfony, Laravel, or WordPress code to use newer patterns.
That said, you should also be aware of Rector’s limitations:
- No Business Logic Understanding: Rector won’t know if your custom
strposwrapper function should be updated differently. - Configuration Complexity: For large codebases, you’ll likely need custom rule configurations and possibly even custom Rector rules.
- Not 100% Safe: While Rector is generally reliable, you should always review changes and run your test suite. Edge cases exist.
- Learning Curve: Understanding what rules do and how to combine them takes time.
For many teams, though, the time savings are substantial. What might take a developer weeks can happen in minutes—with the confidence that the changes are applied consistently across the entire codebase.
Alternatives to Rector
Of course, Rector isn’t the only way to upgrade PHP versions. Before we dive into using it, let’s acknowledge what else is out there:
Manual Refactoring: You could update everything by hand. This gives you complete control, but it’s error-prone and terribly inefficient for large codebases. It also doesn’t scale—if you have 100 files with deprecated patterns, you’ll miss some.
PHP_CodeSniffer with Custom Sniffs: PHP_CodeSniffer can detect deprecated usage, but it doesn’t automatically fix code. You’d still need to manually apply fixes or write custom fixers. It’s more of a linter than a refactoring tool.
PHP-CS-Fixer: This tool focuses on coding style (like PSR-12) and some simple refactorings. It’s excellent for formatting but doesn’t have the depth of PHP version migration rules that Rector provides.
Custom Scripts: You could write your own scripts using PHP-Parser (the library Rector is built on). This gives ultimate flexibility but requires significant expertise and maintenance overhead.
In practice, Rector has become the de facto standard for automated PHP upgrades because it combines breadth of rules with practical safety. For most teams, it’s the most efficient path forward.
Installation and Basic Configuration
Let’s walk through setting up Rector for a basic PHP version upgrade. We’ll start with the simplest possible scenario and build from there.
First, add Rector to your project as a development dependency:
composer require rector/rector --dev
That’s it, of course—Rector is now installed. Next, we need to tell it what to do. Create a file named rector.php in your project root:
<?php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
// Directories Rector should analyze
$rectorConfig->paths([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
// Apply upgrades for PHP 8.1
$rectorConfig->sets([
SetList::PHP_81,
]);
};
This configuration tells Rector to analyze your src and tests directories and apply all rules from the PHP 8.1 set. You can adjust the paths and version as needed.
Running Rector Safely
Before applying any changes, you should see what Rector plans to do. Always run a dry run first:
vendor/bin/rector process --dry-run
Rector will output a diff showing all proposed changes but won’t modify any files. This is your chance to review the changes. You might see hundreds of modifications—that’s normal.
Tip: Rector can also output changes in other formats. For a summary without seeing the full diff:
vendor/bin/rector process --dry-run --output-format=json > rector-changes.json
That command saves a structured report of all planned changes to a JSON file, which you can inspect programmatically if needed.
Applying Changes
Once you’re satisfied with the dry run output, apply the changes:
vendor/bin/rector process
Rector will now modify your files. Let’s look at a concrete example of what you might see.
Before Rector
<?php
class UserService
{
private $name;
private $email;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
public function getName(): string
{
return $this->name;
}
}
After Rector (with PHP 8.0+)
<?php
class UserService
{
public function __construct(
private $name,
private $email
) {
}
public function getName(): string
{
return $this->name;
}
}
Rector automatically converts traditional constructor property assignments to constructor property promotion—a PHP 8.0 feature that reduces boilerplate. This is just one of hundreds of transformations Rector can perform.
Verifying the Upgrade
After Rector finishes, you should verify that your application still works. At minimum:
- Run your test suite:
vendor/bin/phpunitorvendor/bin/pest - Check for syntax errors:
php -l src/or usephp -d display_errors=1 -r "require 'src/your-file.php';" - Start your development server and manually test critical paths
Rector is reliable, but it’s not infallible. Complex code with reflection, variable variables, or dynamic method calls might confuse Rector’s transformations. That’s why your test suite is essential—it’s your safety net.
Going Deeper: Custom Rules and Configuration
For many projects, the basic setup above is sufficient. But complex codebases often need more granular control. Let’s look at a few advanced scenarios.
Excluding Specific Rules
Suppose you want most PHP 8.1 upgrades but need to skip the isset to count rule because it breaks some of your code. You can disable specific rules:
<?php
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/src']);
// Import the PHP 8.1 set...
$rectorConfig->sets([SetList::PHP_81]);
// ...but skip this specific rule
$rectorConfig->skip([
\Rector\Php81\Rector\FuncCall\NonNullableToStringRector::class,
]);
};
Using Multiple Sets Together
You can combine different rule sets. For example, to upgrade to PHP 8.1 AND adopt some code quality improvements:
$rectorConfig->sets([
SetList::PHP_81,
SetList::CODE_QUALITY,
SetList::TYPE_DECLARATION,
]);
Of course, combining sets means more changes to review. Start conservative—you can always add more rules later.
Custom Rule Sets
If you find yourself repeatedly configuring the same rules, you can create your own reusable set. Create rector-custom.php:
<?php
use Rector\Config\RectorConfig;
use Rector\Set\Contract\SetInterface;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
MyProjectSetList::UPGRADE_TO_81_WITH_QUALITY,
]);
};
class MyProjectSetList implements SetInterface
{
public const UPGRADE_TO_81_WITH_QUALITY = 'MyProjectSetList/upgrade-to-81-with-quality';
public function getSetList(): array
{
return [
SetList::PHP_81,
SetList::CODE_QUALITY,
// Plus your custom rules...
];
}
}
This is more advanced, but useful for large organizations with consistent coding standards.
Real-World Considerations
Let’s talk about what happens when you run Rector on a non-trivial codebase.
Incremental Upgrades vs. Big Bang
You can upgrade directly from PHP 7.4 to 8.2 if you want—Rector handles multi-version jumps. Many teams, though, prefer incremental upgrades: 7.4 → 8.0, test, then 8.0 → 8.1, test again. The advantage is easier troubleshooting if something breaks. If you jump directly and hit an issue, you have to untangle which version change caused it.
My recommendation: If your test coverage is good, a direct upgrade is fine. If tests are sparse, consider incremental steps.
Handling Third-Party Code
Rector analyzes everything in your configured paths. That includes vendor code if you accidentally include it. You almost certainly don’t want to modify vendor dependencies—those are managed by Composer and will be updated separately.
Make sure your paths configuration excludes vendor/:
$rectorConfig->paths([
__DIR__ . '/src',
__DIR__ . '/tests',
// Don't include vendor!
]);
Dealing with False Positives
Rector isn’t perfect. Occasionally, it might suggest changes that aren’t appropriate for your specific code. The skip configuration mentioned earlier handles this. You can also write custom rules to handle project-specific patterns—but that’s beyond our scope.
Troubleshooting Common Issues
Tests Fail After Rector
This is the most common problem. Here’s how to debug:
- Look at the specific test failures. Are they related to Rector’s changes?
- Run
vendor/bin/rector process --dry-runagain and review the diff. Did Rector change something unexpected? - Use
git diffto see exactly what changed. Sometimes the cumulative effect of many small changes can interact in surprising ways. - Consider excluding problematic files or rules temporarily and addressing those manually.
Rector Throws Errors
If Rector itself crashes during processing:
- Ensure you’re using a compatible Rector version for your PHP version.
- Check that your
rector.phpconfiguration is valid PHP. - Try running with
--verboseto see more details:vendor/bin/rector process --dry-run --verbose - Search the Rector GitHub issues—someone may have encountered the same problem.
Performance on Large Codebases
Rector processes files sequentially by default. For large projects, you can enable parallel processing by adding:
$rectorConfig->parallelProcesses(4); // Adjust based on CPU cores
This can significantly speed up processing, but uses more memory.
Conclusion: A Pragmatic Approach
Rector is a powerful tool that can transform how your team approaches PHP upgrades. But it’s not a silver bullet. Here’s what we’ve covered:
- Rector automates mechanical refactorings by understanding PHP code structure
- It’s generally reliable but requires review and testing
- Alternatives exist, but Rector is the most comprehensive solution for PHP version upgrades
- Start with a basic configuration and a dry run
- Always verify changes with your test suite
- Use skip rules for edge cases Rector doesn’t handle correctly
The goal isn’t to eliminate manual work entirely—it’s to free your team from tedious, error-prone tasks so they can focus on the complex logic that actually requires human intelligence. Used thoughtfully, Rector makes PHP upgrades less daunting and more routine. That means your application stays secure, performs better, and can use modern language features without the usual pain.
Next steps: Install Rector in a development branch, run a dry run on your codebase, and see what changes it proposes. The results might surprise you.
Integrate Rector into your CI/CD pipeline to catch deprecations early. Run it regularly to keep your codebase incrementally modernized. And remember: the best upgrade is the one that happens before your PHP version becomes end-of-life.
Sponsored by Durable Programming
Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.
Hire Durable Programming