Rector is an open-source tool that automatically rewrites PHP code for a newer language version — it removes deprecated functions, updates syntax and modernizes constructs to current standards. In practice it eliminates 80–90% of the mechanical work of a PHP migration, shortening it from weeks to days. This guide shows how to configure Rector for a migration, how to roll out changes version by version and which pitfalls to avoid.
Key takeaway: Rector does not replace tests or architectural decisions — it is a mechanical refactoring machine. The safe workflow: ruleset for one version step → run with
--dry-run→ review the diff → commit → tests → next step. Never run Rector on production without going through staging.
What Rector is and what it can do
Rector analyzes code through its AST (syntax tree) and rewrites it according to sets of rules. It has three overlapping uses that come together during a PHP migration.
- PHP version upgrades — the
PHP_80,PHP_81,PHP_82,PHP_83,PHP_84rulesets (and the cumulativewithPhpSets) replace removed functions, update syntax and introduce new constructs. - Framework upgrades — ready-made sets for Symfony, Laravel, PHPUnit, Doctrine and dozens of other projects.
- Quality refactoring — typing, dead code, early returns, condition simplification.
Rector supports transitions from PHP 5.3 all the way to the newest versions — it is the only tool of its kind with such broad historical coverage. The official documentation with the full rule list lives in the rectorphp/rector repository on GitHub.
Installation and minimal configuration
Rector installs as a development dependency. For a project with Composer
composer require --dev rector/rector
Configuration is a rector.php file in the project root. A minimal version for a migration to PHP 8.4
<?php
// rector.php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/app',
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets(php84: true); // cumulative: 5.6 → 7.x → 8.0–8.4
The key configuration decisions.
withPhpSets(php84: true)— the cumulative set: Rector applies the rules of every transition leading to 8.4. The fastest route when migrating straight from 5.6/7.x.withPaths— the directories Rector may rewrite. Always include the tests directory — otherwise consistency drifts.withSkip— exceptions for rules that do more harm than good in your project (e.g. aggressive condition simplification in generated code).
Use the stepwise version when you want to go version by version — recommended in large projects
// Stage 1: only the 7.4 → 8.0 transition
return RectorConfig::configure()
->withPaths([__DIR__ . '/src'])
->withPhpSets(php80: true);
When a stage is done, you swap the configuration for the next step (php81: true, then php82: true, and so on) and repeat the cycle.
- ## Workflow: running a Rector migration step by step
The safe process has six steps, repeated for every version stage.
- Make sure the tests pass — a green starting point is a condition. Rector rewrites code en masse; without tests you cannot tell a correct change from a regression.
- Run Rector in preview mode — you see exactly what will change, with no files written.
- Review the diff — the changes may run into thousands of lines, but the patterns repeat. Spot the unusual cases.
- Apply the changes — drop the preview flag so Rector writes the files.
- Run tests and PHPStan — green tests plus no new static errors equals a commit.
- Commit as a separate step — git history then shows exactly which ruleset changed what, which makes rollbacks easy.
The commands look like this
# Preview changes (writes nothing)
vendor/bin/rector process --dry-run
# Apply changes
vendor/bin/rector process
# Only a selected directory
vendor/bin/rector process src/Legacy
Tip: In large projects, run Rector directory by directory or module by module, committing after each. It is easier to locate a failure when tests break — and changes can go to review in parts.
Rector in CI — so code does not "travel back in time"
A one-off migration is just the beginning. Rector's second value is keeping new code from using constructs from previous versions. Add a checking step to your pipeline:
# .github/workflows/ci.yml (fragment)
- name: Rector (dry-run — blocks time travel)
run: vendor/bin/rector process --dry-run
If a developer writes code in the old style, CI catches it. This matters especially in projects migrated in stages — without such a gate, old patterns come back with every new task.
What Rector will and will not fix
It is worth knowing the limits of automation so you do not overestimate the tool.
Rector fixes automatically: removed functions (each(), create_function()), changed function APIs (preg_replace with /e, mysql_* → mysqli/PDO within limits), implicitly nullable parameters, get_class() without an argument, types and syntax (arrow functions, constructor promotion, match), partially property typing.
Rector will not fix: logic relying on behaviors that changed between versions (type casting, sorting, numeric string comparisons), abandoned libraries that must be replaced, architectural problems, code generated by other tools (exclude it with withSkip).
That is why the compatibility audit (PHPCompatibility, PHPStan) and tests stay in the plan — Rector works between them: after the audit, before the regression tests on staging.
Pitfalls worth knowing before the first run
- A diff you cannot fully review — in a 100k-line project the first pass may change 20k lines. Do not try to read it all: review a sample, trust the tests, commit in steps.
- Conflicts with formatting — Rector changes syntax but does not format code. Run
php-cs-fixerorpintafter it so the diff stays readable. - Generated code — compiled templates, caches, ORM-generated migrations: exclude them via
withSkiporwithPaths, or Rector may "fix" something that gets overwritten anyway. - Aggressive rulesets — some sets (e.g.
LevelSetList::UP_TO_PHP_84with quality rules) change more than the migration requires. For migrations usewithPhpSets, not full level sets. - No tests means no safety net — in untested projects, write characterization tests on critical paths first, then run Rector. Otherwise you have no way to verify mass changes.
Rector and the other migration tools
Rector does not work alone — three tools accompany it in a PHP migration process, each with a different role.
| Tool | Role | When you run it |
|---|---|---|
| PHPCompatibility | detects constructs incompatible with the target version | before the migration (audit) |
| Rector | automatically rewrites code for the target version | after the audit, before tests |
| PHPStan | detects type and call errors after the rewrite | after Rector, in CI |
| PHP-CS-Fixer / Pint | formatting after mass changes | after Rector |
This arrangement — detect, rewrite, verify, format — is the standard PHP migration pipeline. The whole process, with the audit and deployment stages, is described in the PHP migration guide. If you are migrating from a specific version, the ruleset order and the most important breaking changes are in the article on the PHP 7.4 to 8.x migration.
When Rector is the right choice — and when it is not
Rector makes the most sense in projects that have tests or at least working critical paths to verify manually. In framework projects (Symfony, Laravel) there is a bonus: ready-made framework rulesets let you combine the PHP migration with a framework upgrade.
I advise against Rector in two situations. First: a project with no tests and no staging environment — mass changes without a safety net are a gamble. Second: code you plan to rewrite from scratch anyway — automating refactoring of code that will disappear is a waste of time. In both cases, protection first (tests, a decision on direction), then tools.
If you want to assess how much work your project faces before running Rector — order a legacy code audit or a technical audit. The audit report contains a blocker list and a migration plan, and Rector is one of the first execution steps.
Frequently asked questions
Is Rector safe for production code?
Rector itself never touches production — it works on files in the repository. The risk is that it rewrites code incorrectly in places it does not understand. Hence the standard workflow: branch, preview (--dry-run), tests, staging, production last. With tests and staging the risk is low.
How much time does Rector save on a PHP migration?
In a typical project it eliminates 80–90% of the mechanical work — replacing removed functions, updating syntax, fixing deprecations. A migration that would take weeks by hand takes days with Rector. The remaining 10–20% (logic depending on changed language behavior, library replacements) still needs a human.
Does Rector work with Laravel and Symfony?
Yes — Rector ships official rulesets for Laravel, Symfony, PHPUnit, Doctrine and others. For Laravel there is a separate framework upgrade set that can be combined with the PHP version rulesets. Note: run the framework upgrade as a separate step (separate commit, separate tests), not together with the PHP migration.
Do I need tests to use Rector?
Formally no — Rector will run without them. Practically yes: with changes counted in thousands of lines, tests are the only verification method. If the project has no tests, write characterization tests on critical paths first (login, checkout, payment). That investment pays back with every subsequent change.
What if Rector changes something incorrectly?
Roll back the specific commit (commit in steps, so the rollback is precise), add a rule to withSkip or exclude the file, and fix the case by hand. Also report the issue in the Rector repository — it is an actively developed project and such reports are usually fixed quickly.
Can Rector handle a jump from PHP 5.6 straight to 8.4?
- **Yes —
withPhpSets(php84:** true)applies the rules of every transition from 5.6 to 8.4 cumulatively. In large projects, though, it is better to go step by step (7.4 → 8.0 → 8.1 → …), because it is easier to attribute errors to a specific change and safer to deploy to staging.