Rector für Legacy-PHP: 80 % der Upgrade-Arbeit automatisieren

#Rector PHP automatisiertes Refactoring
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Frag einen Raum voller PHP-Entwickler, ob sie Rector kennen, und die meisten Hände gehen hoch. Frag, wer es tatsächlich gegen eine Legacy-Codebase in Produktion laufen lassen hat, und der Raum wird still. Rector PHP automatisiertes Refactoring ist die am meisten ungenutzte Fähigkeit im Ökosystem: Ein Tool, das Tausende Dateien in Minuten korrekt umschreiben kann, liegt brach, während Teams Methodensignaturen von Hand aktualisieren.

Das Zögern ist verständlich. Legacy-Projekte sind fragil, Rectors Output kann beim ersten Kontakt überwältigend sein, und niemand will einen Diff über 4.000 Dateien reviewen. Aber die Angst kommt meist daher, Rector falsch auszuführen: alle Rules auf einmal, keine Baseline, kein Review-Workflow. In der richtigen Reihenfolge mit den richtigen Leitplanken automatisiert es zuverlässig den Großteil eines Versions- oder Framework-Upgrades und lässt dein Team sich auf die 20 % konzentrieren, die menschliches Urteilsvermögen brauchen.

Dieser Beitrag geht den Workflow durch, den wir in Legacy-Modernisierungsprojekten nutzen: Rector in einer alten Codebase ohne Dependency-Konflikte installieren, Rule Sets sequenzieren, Custom Rules für projektspezifische Muster schreiben, Änderungen mit Vertrauen reviewen, und wie die Ergebnisse auf einer echten Symfony-3.4-Anwendung tatsächlich aussehen.

Rector in einem Symfony-2-Ära-Projekt installieren, ohne Dependency-Hölle

Das erste Hindernis ist real: Rector braucht PHP 7.4+ und moderne Dependencies, während dein Legacy-Projekt uralte Versionen von allem pinnt. composer require rector/rector --dev in einem Symfony-2- oder -3-Projekt endet meist in einer Wand aus Versionskonflikten.

Die Lösung: Rector gar nicht erst ins Projekt installieren. Du hast zwei saubere Optionen.

Option 1: ein separates Tools-Verzeichnis. Halte Rector in seiner eigenen composer.json, isoliert von der Anwendung:

mkdir -p tools/rector
composer require --working-dir=tools/rector rector/rector
tools/rector/vendor/bin/rector process src --dry-run

Rector parst deinen Code statisch. Es muss ihn nicht ausführen, also muss es den Dependency-Baum deiner Anwendung nicht teilen. Es braucht nur deinen Code und idealerweise deinen Autoloader für die Typauflösung.

Option 2: das PHAR oder Docker-Image. Das offizielle Docker-Image gibt dir eine vollständig isolierte Laufzeitumgebung, was auch das Problem löst, dass Rector eine neuere PHP-Version braucht als die, auf der deine Legacy-App läuft:

docker run --rm -v $(pwd):/project rector/rector:latest process /project/src --dry-run

Ein Konfigurationsdetail ist bei Legacy-Projekten wichtig: Zeige Rector explizit auf deinen Autoloader via autoload_paths (oder bootstrapFiles bei ungewöhnlichen Setups), damit es Typen in Code auflösen kann, der älter ist als PSR-4. Ohne Typinformationen überspringt Rector Rules, die sonst greifen würden, und du verlierst still Abdeckung.

Rule Sets: Die Reihenfolge zählt mehr als die Auswahl

Der häufigste Weg, wie Teams ihren ersten Rector-Lauf sabotieren, ist alles auf einmal anzuwenden: PHP-Upgrade-Rules, Symfony-Rules, Dead-Code-Entfernung und Coding Style in einem einzigen Durchgang. Der Diff wird unreviewbar und der Lauf wird aufgegeben.

Sequenziere die Arbeit stattdessen. Jede Phase bekommt ihren eigenen Lauf, ihren eigenen Review und ihren eigenen Commit:

  1. PHP-Versions-Rules zuerst. Arbeite die LevelSetList-Ziele eine Version nach der anderen ab: 7.1, 7.4, 8.1. Sprachliche Änderungen (Arrow Functions, Null Coalescing, Constructor Promotion, Readonly Properties) sind unabhängig von deinem Framework und liefern die Plattform, auf der jede spätere Rule aufbaut.
  2. Framework-Rules als zweites. Symfony-Rule-Sets setzen moderne PHP-Syntax voraus. SymfonySetList-Upgrades auf Code aus der PHP-5-Ära laufen zu lassen produziert verrauschten, manchmal falschen Output. Auf Code nach dem Versions-Upgrade sind sie präzise: veraltete Controller-Muster konvertieren, Event-Dispatcher-Signaturen aktualisieren, weg von entfernten Basisklassen migrieren.
  3. Type-Declaration-Rules als drittes. Die TypeDeclarationLevel-Rules inferieren Parameter- und Rückgabetypen aus der Verwendung. Sie funktionieren nach den Versions-Upgrades dramatisch besser, weil sie auf moderner Syntax und auf Typen aufbauen können, die die Framework-Rules bereits eingeführt haben.
  4. Dead Code und Code Quality zuletzt. Das sind Aufräumarbeiten, keine Upgrades. Sie getrennt zu halten hält jeden früheren Diff auf eine Frage fokussiert: Ist das Verhalten gleich geblieben?

Eine minimale rector.php für die erste Phase sieht so aus:

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

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src'])
    ->withSets([LevelSetList::UP_TO_PHP_74]);

Widersteh der Versuchung, weitere Sets hinzuzufügen, wenn du schon mal dabei bist. Ein Anliegen pro Lauf ist das, was den Review ehrlich hält.

Custom Rules für die Muster, die nur deine Codebase hat

Jede Legacy-Codebase sammelt Hausmuster an: einen selbstgebauten ArrayHelper::get(), der älter ist als der Null-Coalescing-Operator, statische Service-Locator-Aufrufe wie AppKernel::getContainer()->get('mailer') oder eine eigene Collection-Klasse, die dupliziert, was array_map tut. Kein öffentliches Rule Set kennt sie, aber sie tauchen oft tausendfach auf.

Hier zahlt sich Rector am meisten aus, denn eine Custom Rule sind ein paar Dutzend Zeilen PHP. Eine Rule, die ArrayHelper::get($data, 'key', null) durch $data['key'] ?? null ersetzt, folgt einer einfachen Form: einen Static-Call-Node matchen, Klassen- und Methodennamen prüfen, den Ersatz-Node zurückgeben. Die Rector-Dokumentation behandelt die Node-API, und vendor/bin/rector custom-rule erzeugt das Grundgerüst samt Test.

Zwei Leitlinien aus Projekten, in denen das gut lief:

  • Schreib zuerst ein Test-Fixture. Rector-Rules werden mit Vorher-Nachher-Code-Snippets getestet. Sammle fünf echte Beispiele aus deiner Codebase, inklusive der seltsamen, und mach sie zum Fixture. Die seltsamen sind die Stellen, an denen handgeschriebene Rules brechen.
  • Bevorzuge enge Rules gegenüber cleveren. Eine Rule, die 90 % der Vorkommen sicher behandelt und den Rest überspringt, schlägt eine, die 100 % behandelt und drei davon falsch umschreibt. Alles, was die Rule überspringt, taucht in einem späteren Grep auf und wird von Hand behoben.

Speziell für veralteten Service-Container-Zugriff: Schreib die Rule so, dass sie nur dann zu Constructor Injection konvertiert, wenn die Klasse bereits ein registrierter Service ist, und sonst nur die Fundstelle meldet. Halbautomatisch schlägt falsch automatisiert.

Der Dry-Run- und Diff-Review-Workflow

Vertrauen in Tausende automatisierte Änderungen kommt nicht daher, Rector zu vertrauen. Es kommt aus einem Review-Prozess, der Rector beim Irren erwischen würde:

vendor/bin/rector process src --dry-run

Dann, pro Phase:

  1. Mit --dry-run laufen lassen und die Zusammenfassung überfliegen. Rector listet jede angewendete Rule mit einem Diff. Du reviewst noch nicht Zeile für Zeile; du prüfst, dass die angewendeten Rules die erwarteten Rules sind.
  2. Ein Rule Set anwenden, committen und pro Rule diffen. Ein Commit, der exakt eine Art von Änderung enthält, ist im Schnelldurchlauf reviewbar. Fünfzig Dateien, die alle array() zu [] konvertieren, sind in zwei Minuten freigegeben. Dieselben Dateien gemischt mit Typänderungen brauchen eine Stunde.
  3. Nach jeder Phase die Testsuite laufen lassen. Wenn deinem Legacy-Projekt Tests fehlen, ist das der Moment, zuerst Characterization-Tests um die kritischen Pfade zu ergänzen. Statische Analyse hilft auch: Ein PHPStan-Baseline-Lauf vor und nach jeder Phase fängt Typ-Regressionen, die Tests übersehen.
  4. Die riskanten Rules von Hand stichprobenartig prüfen. Typ-Inferenz-Rules und alles, was Conditionals anfasst, verdient einen echten Blick. Syntax-Konvertierungen nicht.

Teams, die dieser Schleife folgen, mergen Rector-Änderungen täglich. Teams, die alles auf einmal laufen lassen, produzieren einen heroischen Branch, der im Review stirbt.

Vorher und Nachher: eine 40K-Zeilen-Symfony-3.4-Codebase

Die ehrlichen Zahlen aus einem aktuellen Engagement: eine 40.000-Zeilen-Symfony-3.4-Anwendung auf PHP 7.1, Ziel Symfony 6.4 auf PHP 8.2, Teil einer größeren Schritt-für-Schritt-Modernisierung.

Was Rector im Wesentlichen perfekt erledigt hat:

  • Sprachliche Syntax-Upgrades über jede Datei: kurze Arrays, Arrow Functions, Null Coalescing, String-Funktionen, Constructor Promotion. Null Regressionen.
  • Rund 2.300 ergänzte Parameter- und Rückgabetyp-Deklarationen, von denen etwa 40 manuelle Korrektur brauchten, meist dort, wo Legacy-Code tatsächlich gemischte Typen zurückgab.
  • Controller- und Command-Signatur-Migrationen für das Framework-Upgrade, inklusive des Wechsels zu attributbasiertem Routing.
  • Unsere drei Custom Rules eliminierten etwa 1.900 Aufrufe von Haus-Helpern und dem statischen Container-Accessor.

Was Rector komplett verpasst hat und immer verpassen wird:

  • Architekturentscheidungen. Rector konvertiert einen veralteten API-Aufruf zu seinem Ersatz. Es kann nicht entscheiden, dass ein 900-Zeilen-Controller fünf Services sein sollte.
  • Verhaltens-Ambiguität. Wo lose Vergleiche und implizite Casts Bugs versteckt haben, bringt strengerer Code sie ans Licht. Ein Mensch muss entscheiden, welches Verhalten beabsichtigt war.
  • Templates, Konfiguration und die Datenbankschicht. Twig-Deprecations, YAML-Service-Definitionen und Doctrine-Mapping-Änderungen brauchten separates Tooling oder Handarbeit.

Das Fazit: Etwa 80 % der mechanischen Upgrade-Arbeit wurden automatisiert, und der Kalendereffekt war größer, als der Prozentsatz vermuten lässt. Das Team verbrachte seine Zeit mit den interessanten 20 %, also genau dem Teil, in dem Erfahrung zählt und in dem eine Rewrite-oder-Refactor-Entscheidung für einzelne Module manchmal neu aufgerollt wird.

Wo du am Montag anfängst

Nimm deinen kleinsten abgegrenzten Kontext. Installiere Rector isoliert in tools/, lass LevelSetList::UP_TO_PHP_72 (oder was auch immer dein nächster kleiner Schritt ist) mit --dry-run laufen und lies den Diff beim Kaffee. Dieser eine Lauf sagt dir mehr über den Aufwand deines Upgrades als jedes Schätz-Meeting.

Wenn du lieber jemanden, der das ein paar Dutzend Mal gemacht hat, die Rule Sets sequenzieren, die Custom Rules schreiben und die Review-Last im Rahmen halten lässt, ist das genau die Arbeit unseres Code Quality Consulting und unserer Legacy-Modernisierungs-Services. Schreib an hello@wolf-tech.io oder schau dich auf wolf-tech.io um, wie wir das angehen.