5 Common Mistakes in Rector Config and How to Avoid Them

Rector is becoming a standard tool to automate PHP/package upgrades and code quality improvements. Last month, we crossed 60 000 downloads a day.

Past 2 months, we've also improved CPU and memory performance, making Rector a lighter version.

Yet, even fast and lightweight Rector can get stuck on simple config mistakes. We'll talk about the 5 most common ones and how to avoid them.

We used the following tips when upgrading the Rector config in Mautic.

We want to share them with you so you can get the most out of Rector.


1. Use explicit paths over /vendor

This happens very rarely, but it's worth mentioning. Rector should always run only on the code you own. If you run it in the root directory, the memory might bloat on the bare /vendor directory.

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/../first-project',
        __DIR__ . '/../second-project',
    ]);
  • Be sure to install Rector directly to your project using composer, like any other dev package, to avoid such accidents on climbing paths up.

  • Make sure you use the paths you own - including tests and config directory:

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/config',
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ]);

2. Avoid checking migrations and test fixtures

This mistake is hard to spot but effective in throttling your Rector run. Doctrine migrations, test fixtures, and any other generated generated PHP files should be excluded. Why? At the start, you can have 5-10 database migrations - that's fine, but the older the project is, the greater the burden to handle on every run.

We've seen projects with 200-600 migration files carefully hidden in /src/Migrations like any other production file. These files should be excluded not only for performance gains but also to make sure their structure and behavior will persist.

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withSkip([
        __DIR__ '/app/Migrations',
    ]);

Moving those files into the root /migrations directory is even better, so Rector and other tools like PHPStan and ECS do not check them.


3. Avoid keeping UP_TO_* for longer than needed

Symfony, PHPUnit and Twig level sets caused mutually conflicting changes and heavy performance loads. They got deprecated since Rector 0.19.2. Use the last major version set instead.

Rector can handle both code improvements and package upgrades. The upgrade is usually a one-time job to get your codebase to the latest PHP and packages. You can find the following sets in your code:

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withSets([
        LevelSetList::UP_TO_PHP_81,
        // deprecated since 0.19.2
        PHPUnitLevelSetList::UP_TO_PHPUNIT_100,
        SymfonyLevelSetList::UP_TO_SYMFONY_63,
    ]);

These are what we call low-hit sets. They contain dozens of rules that will never find any code to upgrade or change, yet they're still run on every Rector run. That's like checking every release of The Time magazine for prime numbers higher than 1,000,000 - it's a waste of your time and resources.

For, the Symfony 6.3 level set itself contains 80 rules - including a rule that renames class and checks class renames in a nested manner.

How should we use these UP_TO_* sets then?

Enable them just once during the upgrade period. Let's say we are upgrading to Symfony 6.3 - we keep the set in rector.php for the time being, and once we're on Symfony 6.3, we remove it.

Don't worry; any leftovers will be reported again in 6 months when you handle the Symfony 6.4/7 upgrade.


4. Instead of long withRules() calls, use slim withPreparedSets()

During the upgrade period, it's also typical to add one rule at a time, run Rector, and push the fixed cases. Then repeat. That way, you might end up with 100+ rules listed one by one from a single set:

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withRules([
        \Rector\DeadCode\Rector\BooleanAnd\RemoveAndTrueRector::class,
        \Rector\DeadCode\Rector\Stmt\RemoveUnreachableStatementRector::class,
        \Rector\DeadCode\Rector\ClassConst\RemoveUnusedPrivateClassConstantRector::class,
        \Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector::class,
        \Rector\DeadCode\Rector\Concat\RemoveConcatAutocastRector::class,
        \Rector\DeadCode\Rector\Return_\RemoveDeadConditionAboveReturnRector::class,
        \Rector\DeadCode\Rector\For_\RemoveDeadContinueRector::class,
        \Rector\DeadCode\Rector\For_\RemoveDeadIfForeachForRector::class,
        \Rector\DeadCode\Rector\If_\RemoveDeadInstanceOfRector::class,
    ]);

This makes rector.php hard to read and maintain. Instead, use the whole set to keep it slim:

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPreparedSets(deadCode: true);

Are there rules you don't like from a particular set? You can skip them.


5. Make use of code quality sets

Last but not least, make sure you're using the most powerful feature of Rector. It's not the upgrade sets but the refactoring sets.

Those sets are the opposite of the low-hit sets discussed in point 3. They're helping you with everyday coding - in PHP 7.0, PHP 8.2, Laravel, Symfony, or in plain PHP.

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPreparedSets(deadCode: true, codeQuality: true, naming: true, privatization: true);

Avoid adding them all at once, as such a PR is impossible to review. Instead, add one set by another and push the fixes in between.


Happy coding!