6 min read

Rector: Stop Manually Upgrading PHP — Let the Robot Do It

Rector automates PHP version upgrades and refactoring. Learn how to set it up, write custom rules, and integrate it into your CI pipeline.

Featured image for "Rector: Stop Manually Upgrading PHP — Let the Robot Do It"

Every PHP developer has been here: a major version upgrade lands, the changelog is long, the deprecation list is longer, and you are staring at a codebase with ten thousand lines of code that needs to be touched in ways that are tedious, mechanical, and almost entirely automatable. You update a few files, miss a few more, ship it, and watch the warning log fill up with things you forgot.

Rector exists to take this entire category of work off your plate. It is a tool for automated PHP refactoring and upgrade migrations — a static analysis-powered engine that reads your code, applies transformation rules, and rewrites it for you. After years of steady development under Tomáš Votruba and the open-source community around it, it has become one of the most valuable tools in the PHP ecosystem, and still one of the most underused.

Here is a practical introduction to getting Rector working for you.

What Rector Actually Does

Rector parses your PHP files into an abstract syntax tree (AST), matches nodes against a library of rules, and emits modified source code when a rule fires. The transformation is precise: it does not do text substitution, it manipulates the tree. This means changes are syntactically valid and aware of context in ways that find-and-replace never could be.

The rule library covers two main categories. First, PHP version upgrades: rules for every deprecation and removal from PHP 7.1 through PHP 8.5 — things like replacing each(), updating typed property syntax, converting strpos() !== false to str_contains(), and automatically adding #[Override] attributes where applicable. Second, code quality rules: fixes for common code smells, dead code removal, simplification of complex conditionals, and framework-specific refactors for Laravel, Symfony, Doctrine, and others.

Installing and Running It

composer require rector/rector --dev

Once installed, generate a base config:

./vendor/bin/rector init

This creates rector.php in your project root. Open it and tell Rector which paths to process and which rule sets to apply:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/app',
        __DIR__ . '/tests',
    ])
    ->withPhpSets(php84: true)
    ->withSets([
        SetList::CODE_QUALITY,
        SetList::DEAD_CODE,
    ]);

The withPhpSets(php84: true) call enables all the rules required to modernize code to PHP 8.4 idioms. Swap in php85: true once your environment is ready for it.

Run a dry-run first — this shows you what Rector would change without touching your files:

./vendor/bin/rector --dry-run

When you are happy with the diff, apply it:

./vendor/bin/rector

Commit the result as a single mechanical commit. Reviewers will thank you for keeping it separate from any intentional behavior changes.

A Real Example: Property Hooks in PHP 8.4

PHP 8.4 introduced property hooks, which let you define get and set logic directly on a property instead of writing boilerplate accessor methods. Rector’s ClassPropertyAssignToConstructorPromotionRector and related rules handle a lot of the class modernization automatically. For the property hook pattern specifically, custom rules close the gap.

Here is what a Rector custom rule looks like in structure:

<?php

declare(strict_types=1);

namespace App\Rector;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;

final class RemoveRedundantGetterSetterRector extends AbstractRector
{
    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(
            'Remove simple getter/setter pairs in favor of public properties',
            [
                new CodeSample(
                    'private string $name; public function getName(): string { return $this->name; }',
                    'public string $name;'
                ),
            ]
        );
    }

    /**
     * @return array<class-string<Node>>
     */
    public function getNodeTypes(): array
    {
        return [Class_::class];
    }

    public function refactor(Node $node): ?Node
    {
        // Your AST transformation logic here
        // Return null if no changes, return $node if modified
        return null;
    }
}

Custom rules plug into the same pipeline as built-in ones. Wire yours up in rector.php:

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withRules([
        \App\Rector\RemoveRedundantGetterSetterRector::class,
    ]);

Writing a rule requires understanding the AST node types that PHP-Parser provides. The PHP-Parser documentation and Rector’s own source are the best references. Once you get comfortable with it, you can encode any mechanical refactoring your team keeps doing by hand.

Integrating with CI

Rector in CI works best as a linter rather than an auto-committer. Add a job that runs the dry-run and fails if Rector would make any changes:

# .github/workflows/rector.yml
name: Rector

on: [push, pull_request]

jobs:
  rector:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
      - run: composer install --no-progress --prefer-dist
      - run: ./vendor/bin/rector --dry-run

If a developer introduces code that violates your configured rules, CI catches it. The fix is then either to update the code or to consciously exclude the file from Rector’s scope.

Practical Tips Before You Ship

Skip problematic files selectively. Some generated code or third-party stubs should be left alone:

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/app'])
    ->withSkip([
        __DIR__ . '/app/Generated',
        \Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector::class => [
            __DIR__ . '/app/Legacy',
        ],
    ]);

Run incrementally on large codebases. If your codebase is large, start by enabling only the PHP version set and committing that. Add code quality sets in a follow-up PR. Mixing mechanical and quality changes in one diff makes review much harder than it needs to be.

Pair it with PHPStan. Rector rewrites, PHPStan verifies. Run PHPStan after every Rector pass to catch any edge case where a transformation produced valid syntax but broke type contracts. This combo covers the gap between “it compiles” and “it is correct.”

Check the community rulesets. The driftingly/rector-laravel and rectorphp/rector-symfony packages add framework-specific rules that the core library does not ship. If you are on Laravel or Symfony, install the relevant package and enable its sets.

The Real Value

The honest pitch for Rector is not that it handles every upgrade perfectly. It does not. Edge cases exist, and you will still read every diff before committing. The real value is that it handles ninety percent of the mechanical work — the str_contains substitutions, the nullable type annotations, the constructor promotion conversions — so that the ten percent you review manually is actually interesting and worth your attention.

For a codebase that has been running PHP 7.x and is finally moving to 8.4 or 8.5, Rector is the difference between a multi-week migration slog and a focused two-day effort. That trade is always worth making.

Resources