Rector: automatisiertes Refactoring bei PHP-Versionswechseln
AI generated
<?php
8.4
PHP · Rector · Refactoring · PHP 8.4
Rector: automatisiertes Refactoring bei PHP-Versionswechseln
von PHP 7.4 zu PHP 8.4 ohne manuelles Suchen-und-Ersetzen

Rector automatisiertes Refactoring ersetzt manuelles Suchen-und-Ersetzen bei PHP-Versionswechseln durch AST-basierte Codeumschreibung: Constructor Property Promotion, readonly-Properties, match-Expressions und der Nullsafe-Operator werden mechanisch und wiederholbar auf eine ganze Codebasis angewendet, inklusive Dry-Run-Vorschau, eigenen Regeln und CI-Integration gegen Regressionen.

18 Min. Lesezeit rector.php · LevelSetList · PHP 8.4 Set · Custom Rules PHP 7.4 bis 8.4 · CI/CD

1. Was Rector ist und wie es sich von PHPStan und PHP-CS-Fixer unterscheidet

Rector automatisiertes Refactoring bedeutet, dass ein Werkzeug PHP-Code nicht als Text, sondern als abstrakten Syntaxbaum (AST) versteht und gezielt umschreibt. Rector parst jede Datei mit nikic/php-parser in einen AST, wendet eine oder mehrere Regeln auf einzelne Knoten an und schreibt den veränderten Baum wieder als lesbaren PHP-Code zurück. Der entscheidende Unterschied zu einem Suchen-und-Ersetzen-Skript: Rector kennt Typen, Scope und Kontext eines Knotens, bevor es ihn verändert, und trifft dadurch Entscheidungen, die ein reines Textmuster nicht treffen kann.

Rector wird häufig mit PHPStan und PHP-CS-Fixer verwechselt, löst aber ein anderes Problem. PHPStan analysiert Code statisch und meldet Probleme, verändert aber keine einzige Zeile, die Behebung bleibt Handarbeit. PHP-CS-Fixer normalisiert Formatierung, Einrückung und Klammerstellung, rührt aber nicht an der Logik oder Struktur des Codes. Rector automatisiertes Refactoring geht einen Schritt weiter: Es schreibt die eigentliche Struktur um, etwa wenn eine switch-Anweisung durch eine match-Expression ersetzt oder ein klassischer Konstruktor in Constructor Property Promotion überführt wird.

Für Teams, die eine Codebasis von PHP 7.4 auf PHP 8.4 heben müssen, ist genau das der entscheidende Vorteil. Statt hunderte Dateien manuell zu durchsuchen und Muster einzeln zu ersetzen, wendet Rector die passenden Regeln mechanisch und wiederholbar auf die gesamte Codebasis an. Das Ergebnis ist deterministisch: Derselbe Input erzeugt immer denselben Output, was bei manuellen Änderungen durch mehrere Entwickler kaum zu garantieren ist.

2. rector.php konfigurieren: Regelsets, LevelSetList und das PHP-8.4-Set

Der Ausgangspunkt für Rector automatisiertes Refactoring ist die Datei rector.php im Projektroot. Sie gibt ein RectorConfig-Objekt zurück, das über eine Fluent-Interface-API konfiguriert wird: withPaths() legt fest, welche Verzeichnisse verarbeitet werden, withSkip() schließt einzelne Dateien oder Regeln aus, und withPhpSets() aktiviert das Regelset für eine bestimmte PHP-Zielversion. In älteren Rector-Versionen übernahm die Klasse LevelSetList diese Aufgabe über withSets([LevelSetList::UP_TO_PHP_84]), die aktuelle API bündelt das kompakter in withPhpSets(php84: true).

Neben dem PHP-8.4-Set gibt es vorbereitete Sets für Dead-Code-Entfernung (deadCode), Type-Declaration-Vervollständigung (typeDeclarations) und Sichtbarkeits-Verschärfung (privatization), die über withPreparedSets() gemeinsam aktiviert werden. Wer zusätzlich eine projektspezifische Regel anwenden will, hängt sie über withRules() an, ohne das restliche Set zu verändern. Diese Kombinierbarkeit ist einer der Gründe, warum Rector automatisiertes Refactoring sich in bestehende Projekte integrieren lässt, ohne dass man sofort ein komplettes Regelwerk übernehmen muss.


<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;

// rector.php - main configuration entry point
return RectorConfig::configure()
    ->withPaths([
        __DIR__ . '/src',
        __DIR__ . '/tests',
    ])
    ->withSkip([
        __DIR__ . '/src/Legacy/OldBootstrap.php',
        // Skip a single rule for a specific path only
        \Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class => [
            __DIR__ . '/src/Legacy',
        ],
    ])
    // Target rule set: rewrite constructs up to PHP 8.4
    ->withPhpSets(php84: true)
    // Additional prepared sets: dead code, type declarations, visibility
    ->withPreparedSets(
        deadCode: true,
        typeDeclarations: true,
        privatization: true,
    )
    ->withRules([
        \Rector\CodingStyle\Rector\Class_\AddArrayDefaultToArrayPropertyRector::class,
    ])
    ->withCache(cacheDirectory: __DIR__ . '/var/rector-cache');

3. Dry-Run vs. Apply-Modus: sichere Ausführung

Rector automatisiertes Refactoring kennt zwei grundlegende Ausführungsmodi: den Dry-Run- und den Apply-Modus. Im Dry-Run-Modus (--dry-run) analysiert Rector jede Datei, wendet die konfigurierten Regeln gedanklich an und zeigt den resultierenden Diff im Terminal an, ohne eine einzige Datei zu verändern. Das ist der sichere Standardweg, um vor jeder größeren Änderung zu sehen, wie viele Dateien betroffen wären und wie die konkreten Änderungen aussehen, bevor überhaupt etwas geschrieben wird.

Erst wenn der Dry-Run-Diff geprüft und für sinnvoll befunden wurde, folgt der Apply-Modus ohne das Flag, der die Änderungen tatsächlich in die Dateien schreibt. Rector cacht dabei den Parse-Zustand jeder Datei anhand eines Hash-Werts, um wiederholte Läufe zu beschleunigen. Nach einer Änderung an der rector.php-Konfiguration selbst kann dieser Cache veraltete Ergebnisse liefern, weshalb --clear-cache in solchen Fällen Pflicht ist, um den Cache explizit zu invalidieren und einen sauberen Neustart zu erzwingen.


# Preview changes without touching any file
vendor/bin/rector process --dry-run

# After a config change: invalidate the file-hash cache first
vendor/bin/rector process --dry-run --clear-cache

# Review the proposed diff manually
git diff --stat

# Apply the reviewed changes for real
vendor/bin/rector process

# Run the test suite immediately to confirm behavior is unchanged
vendor/bin/phpunit

# Stage and commit in reviewable chunks
git add -p
git commit -m "Rector: apply PHP 8.4 rule set to src/Domain"

4. Konkrete Transformation: von PHP 7.4 zu PHP 8.4

Ein konkretes Beispiel zeigt, wie tiefgreifend Rector automatisiertes Refactoring einen Klassenkörper verändern kann. Eine typische PHP-7.4-Klasse deklariert private Properties, weist sie im Konstruktor Zeile für Zeile zu, nutzt eine switch-Anweisung zur Statusauswertung und prüft verschachtelt auf null, bevor auf ein verschachteltes Objekt zugegriffen wird. Jede dieser drei Stellen hat in PHP 8.0 bis 8.4 eine kürzere, sicherere Entsprechung, die Rector automatisch erkennt und einsetzt.

Die entsprechende PHP-8.4-Regel ersetzt die manuelle Zuweisung im Konstruktor durch Constructor Property Promotion mit readonly-Modifier, verwandelt die switch-Anweisung in eine match-Expression mit erzwungener Vollständigkeit, und ersetzt die verschachtelte Null-Prüfung durch den Nullsafe-Operator ?->. Aufrufer der Klasse können zusätzlich von Named Arguments profitieren, etwa new OrderProcessor(logger: $logger, currency: 'EUR'), was Rector zwar nicht erzwingt, aber durch die promovierten, klar benannten Parameter erst praktikabel macht.


// BEFORE: PHP 7.4 style
class OrderProcessor
{
    private LoggerInterface $logger;
    private PaymentGatewayInterface $gateway;
    private string $currency;

    public function __construct(
        LoggerInterface $logger,
        PaymentGatewayInterface $gateway,
        string $currency
    ) {
        $this->logger = $logger;
        $this->gateway = $gateway;
        $this->currency = $currency;
    }

    public function statusLabel(int $status): string
    {
        switch ($status) {
            case self::STATUS_NEW:
                $label = 'new';
                break;
            case self::STATUS_PAID:
                $label = 'paid';
                break;
            case self::STATUS_SHIPPED:
                $label = 'shipped';
                break;
            default:
                $label = 'unknown';
        }
        return $label;
    }

    public function customerCity(?Customer $customer): ?string
    {
        if ($customer === null) {
            return null;
        }
        $address = $customer->getAddress();
        if ($address === null) {
            return null;
        }
        return $address->getCity();
    }
}

// AFTER: PHP 8.4 style, generated by Rector
final class OrderProcessor
{
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly PaymentGatewayInterface $gateway,
        private readonly string $currency,
    ) {
    }

    public function statusLabel(int $status): string
    {
        return match ($status) {
            self::STATUS_NEW => 'new',
            self::STATUS_PAID => 'paid',
            self::STATUS_SHIPPED => 'shipped',
            default => 'unknown',
        };
    }

    public function customerCity(?Customer $customer): ?string
    {
        return $customer?->getAddress()?->getCity();
    }
}

5. Dead-Code-Entfernung und Type-Declaration-Sets

Neben reinen Versions-Upgrades deckt Rector automatisiertes Refactoring auch Aufräumarbeiten ab, die in gewachsenen Codebasen liegen bleiben. Das deadCode-Set erkennt zum Beispiel private Methoden, die nirgends aufgerufen werden, Properties, die nie gelesen werden, und Bedingungen, die aufgrund bekannter Typen nie wahr werden können. Solche toten Pfade werden nicht nur gemeldet wie bei PHPStan, sondern direkt entfernt, sofern das Entfernen nachweislich sicher ist.

Das typeDeclarations-Set ergänzt fehlende Parameter-, Rückgabe- und Property-Typen anhand von Docblocks, Default-Werten und Aufrufkontext. Eine Methode, deren Rückgabetyp bisher nur im @return-Kommentar stand, bekommt die native Typdeklaration im Methodenkopf. Das privatization-Set verschärft Sichtbarkeiten, wo eine Property oder Methode nur innerhalb der eigenen Klasse verwendet wird, und macht sie von public oder protected zu private. Zusammen reduzieren diese Sets die Angriffsfläche und die kognitive Last beim Lesen einer Klasse spürbar.

6. Eine eigene Rector-Regel schreiben

Die vorgefertigten Sets decken die meisten Standardfälle ab, aber projektspezifische Muster erfordern eine eigene Regel. Rector automatisiertes Refactoring erlaubt genau das über die Klasse AbstractRector: Eine eigene Regel implementiert getNodeTypes(), um zu deklarieren, welche AST-Knotentypen sie besuchen will, und refactor(), um den eigentlichen Umbau vorzunehmen. Gibt refactor() null zurück, bleibt der Knoten unverändert, gibt es den veränderten Knoten zurück, ersetzt Rector ihn im Baum.

Die Methode getRuleDefinition() liefert eine maschinenlesbare Beschreibung mit Vorher-Nachher-Codebeispiel, die Rector sowohl für die automatisch generierte Dokumentation als auch für Tests der eigenen Regel nutzt. Ein typischer Anwendungsfall: Ein internes Logger-Interface benennt eine veraltete Methode warn() in warning() um, und statt diesen Aufruf in hundert Dateien manuell zu suchen, übernimmt eine kleine, getestete Rector-Regel die Umbenennung projektweit.


<?php

declare(strict_types=1);

namespace App\Rector;

use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
 * Custom rule: renames deprecated Logger::warn() calls to Logger::warning()
 */
final class RenameLegacyLoggerMethodRector extends AbstractRector
{
    public function getRuleDefinition(): RuleDefinition
    {
        return new RuleDefinition(
            'Renames deprecated Logger::warn() calls to Logger::warning()',
            [
                new CodeSample(
                    '$logger->warn("message");',
                    '$logger->warning("message");'
                ),
            ]
        );
    }

    public function getNodeTypes(): array
    {
        return [MethodCall::class];
    }

    public function refactor(Node $node): ?Node
    {
        if (! $node instanceof MethodCall) {
            return null;
        }

        if (! $this->isName($node->name, 'warn')) {
            return null;
        }

        $node->name = new Identifier('warning');

        return $node;
    }
}

7. Große automatisierte Diffs sicher ausrollen

Ein einzelner Rector-Lauf über eine große Legacy-Codebasis kann tausende Zeilen in einem einzigen Diff verändern, was Code-Reviews praktisch unmöglich macht. Der sichere Weg besteht darin, Rector automatisiertes Refactoring in kleinen, nachvollziehbaren Schritten auszuführen: withPaths() auf ein einzelnes Modul oder Verzeichnis begrenzen, den Dry-Run-Diff für genau diesen Ausschnitt prüfen, anwenden, testen, committen, und erst danach zum nächsten Verzeichnis übergehen.

Nach jedem Chunk sollte die vollständige Testsuite laufen, nicht nur ein Teilausschnitt, weil Rector-Regeln gelegentlich Randfälle berühren, die in unabhängigen Tests sichtbar werden. Bei Konfigurationsänderungen an der rector.php-Datei selbst ist --clear-cache Pflicht, da der Datei-Hash-Cache sonst veraltete Analyseergebnisse liefert und Änderungen unterschlägt. Ein Feature-Branch pro Chunk mit eigenem Pull Request hält die Diffs überschaubar und erlaubt es Reviewern, sich auf semantische statt auf mechanische Änderungen zu konzentrieren.

8. CI-Integration: Regressionen gegen alte Patterns verhindern

Ohne Absicherung in der CI-Pipeline schleichen sich alte Muster nach einem einmaligen Rector-Lauf schnell wieder ein: Ein neuer Pull Request fügt eine klassische switch-Anweisung statt einer match-Expression hinzu, oder ein Entwickler schreibt einen Konstruktor ohne Property Promotion. Rector automatisiertes Refactoring lässt sich genau dafür in die CI-Pipeline integrieren, indem derselbe Befehl, der lokal für die Migration genutzt wurde, als Prüfschritt mit --dry-run läuft.

Meldet Rector im Dry-Run-Modus einen ausstehenden Diff, bedeutet das, dass mindestens eine Datei vom konfigurierten Regelsatz abweicht, und der Pipeline-Schritt sollte mit einem Nicht-Null-Exit-Code fehlschlagen. So verhindert Rector automatisiertes Refactoring nicht nur die einmalige Migration, sondern auch den schleichenden Rückfall in alte Patterns, ganz ohne dass ein Reviewer jede switch-Anweisung von Hand suchen muss.


name: rector-check
on: [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-interaction --prefer-dist

      # Fail the build if any file still needs a Rector rewrite
      - run: vendor/bin/rector process --dry-run --clear-cache

      - run: vendor/bin/phpunit

9. Rector im Vergleich zu manuellem Refactoring

Rector, PHPStan und PHP-CS-Fixer werden in Projekten oft gemeinsam eingesetzt, weil sie unterschiedliche Ebenen desselben Qualitätsproblems abdecken. PHP-CS-Fixer normalisiert ausschließlich die Formatierung, PHPStan findet Typ- und Logikfehler, ohne sie zu beheben, und Rector ist das einzige der drei Werkzeuge, das die Struktur des Codes tatsächlich umschreibt. Die folgende Tabelle stellt Rector automatisiertes Refactoring dem klassischen manuellen Refactoring direkt gegenüber.

Kriterium Manuelles Refactoring Rector automatisiertes Refactoring Effekt
Geschwindigkeit Tage bis Wochen bei tausenden Dateien Minuten für die gesamte Codebasis Migration wird planbar statt aufwandsabhängig
Konsistenz Abhängig von Person und Tagesform Deterministisch, dieselbe Regel überall Kein Stilbruch zwischen Modulen
Risiko bei großen Diffs Hoch, schwer reviewbar Reduzierbar durch Chunking und Tests Reviewbare, nachvollziehbare Schritte
Reversibilität Kein einheitliches Rollback-Muster Git-Commit pro Chunk, klar revertierbar Einzelne Regeln lassen sich isoliert zurücknehmen
Skalierung auf Legacy-Code Sinkt mit Projektgröße rapide Bleibt bei wachsender Codebasis konstant Auch Codebasen mit Millionen Zeilen migrierbar

Der Geschwindigkeitsunterschied wird besonders bei Versions-Upgrades sichtbar: Wo ein Team für eine manuelle Migration von PHP 7.4 auf PHP 8.4 Wochen einplant, verarbeitet Rector dieselbe Codebasis in Minuten und liefert dabei einen reproduzierbaren, review-baren Diff statt einer Reihe unabhängiger, potenziell inkonsistenter Handänderungen.

10. Zusammenfassung

Rector automatisiertes Refactoring löst ein Problem, das reines Suchen-und-Ersetzen und reine Analyse-Tools wie PHPStan nicht lösen können: die mechanische, AST-basierte Umschreibung von Code über eine ganze Codebasis hinweg. Die rector.php-Konfiguration mit withPhpSets() und withPreparedSets() bündelt Regeln für PHP-Versions-Upgrades, Dead-Code-Entfernung und Typdeklarationen in einer einzigen, versionierbaren Datei. Der Dry-Run-Modus macht jede Änderung vor der Anwendung sichtbar, während --clear-cache verhindert, dass ein veralteter Cache falsche Ergebnisse liefert.

Eigene Rector-Regeln über AbstractRector erweitern das Werkzeug um projektspezifische Refactorings, die kein vorgefertigtes Set abdeckt. Große Migrationen laufen sicher in kleinen, getesteten Chunks statt in einem einzigen riesigen Diff, und die Integration von Rector automatisiertes Refactoring in die CI-Pipeline mit --dry-run verhindert, dass alte Muster nach der Migration schleichend zurückkehren. Zusammen ersetzt das eine Disziplin, die früher ausschließlich auf manueller Sorgfalt beruhte, durch einen wiederholbaren, automatisierten Prozess.

Rector automatisiertes Refactoring, das Wichtigste auf einen Blick

Konfiguration

rector.php mit withPhpSets() und withPreparedSets() bündelt PHP-8.4-Regeln, Dead-Code-Entfernung und Typdeklarationen in einer Datei.

Sichere Ausführung

Dry-Run vor jedem Apply, --clear-cache nach Config-Änderungen, vollständige Tests nach jedem Chunk.

Custom Rules

AbstractRector mit getNodeTypes() und refactor() für projektspezifische Refactorings, die kein Set abdeckt.

CI-Integration

rector --dry-run als Pipeline-Schritt verhindert den Rückfall in alte Patterns nach der Migration.

11. FAQ: Rector automatisiertes Refactoring

1Was ist Rector automatisiertes Refactoring genau?
Rector liest PHP-Code als abstrakten Syntaxbaum und schreibt ihn nach konfigurierbaren Regeln um, statt nur Text zu durchsuchen. Es automatisiert Versions-Upgrades, Dead-Code-Entfernung und Typdeklarationen reproduzierbar über die gesamte Codebasis.
2Wie unterscheidet sich Rector von PHPStan?
PHPStan meldet Probleme statisch, verändert aber keine Zeile Code. Rector schreibt die betroffenen Konstrukte direkt um, etwa fehlende Typdeklarationen, die PHPStan nur anmahnen würde.
3Wie unterscheidet sich Rector von PHP-CS-Fixer?
PHP-CS-Fixer normalisiert nur Formatierung. Rector verändert die tatsächliche Struktur, etwa switch zu match, was über reine Stilkorrekturen hinausgeht.
4Dry-Run vs. Apply-Modus?
--dry-run zeigt den Diff im Terminal ohne Dateien zu verändern. Ohne dieses Flag schreibt Rector die Änderungen tatsächlich. Dry-Run vor jeder größeren Anwendung prüfen.
5Wozu dient --clear-cache?
Rector cacht den Parse-Zustand jeder Datei per Hash. Nach einer Config-Änderung liefert der Cache sonst veraltete Ergebnisse, --clear-cache erzwingt einen sauberen Neustart.
6Wie schreibe ich eine eigene Rector-Regel?
AbstractRector erweitern, getNodeTypes() für die Zielknoten und refactor() für den Umbau implementieren. getRuleDefinition() liefert das Vorher-Nachher-Codebeispiel für Doku und Tests.
7Große Diffs sicher ausrollen?
Auf einzelne Module begrenzen, Dry-Run prüfen, anwenden, volle Testsuite laufen lassen, committen. Kleine, getestete Chunks statt eines riesigen Diffs.
8Kann Rector Bugs einführen?
Ja, bei komplexen Konstrukten. Deshalb nach jedem Chunk die volle Testsuite laufen lassen und den Dry-Run-Diff vor jeder Anwendung prüfen.
9Wie integriere ich Rector in CI?
Denselben Migrationsbefehl als Pipeline-Schritt mit --dry-run ausführen. Ein ausstehender Diff lässt den Build mit Nicht-Null-Exit-Code fehlschlagen.
10Ersetzt Rector Code-Reviews?
Nein. Rector automatisiert die mechanische Umsetzung, der resultierende Diff sollte weiterhin von einem Menschen geprüft werden, besonders bei Custom-Regeln.

Mironsoft

PHP-Versionsupgrades, Legacy-Modernisierung und CI-Absicherung

Eine Codebasis auf PHP 8.4 heben, ohne wochenlange Handarbeit?

Wir konfigurieren rector.php für euer Projekt, schreiben projektspezifische Custom-Regeln und rollen große automatisierte Diffs sicher in kleinen, getesteten Schritten aus, inklusive CI-Absicherung gegen den Rückfall in alte Patterns.

Konfiguration

rector.php mit passenden PHP-8.4-Regelsets und projektspezifischen Skip-Regeln aufsetzen

Custom Rules

Eigene Rector-Regeln für projektspezifische Refactorings entwickeln und testen

CI-Integration

rector --dry-run in die Pipeline integrieren und Regressionen dauerhaft verhindern