Legacy PHP Code schrittweise modernisieren: Strangler Fig statt Big Bang
AI generated
<?php
8.4
PHP · Legacy Code · Refactoring · Modernisierung
Legacy PHP Code schrittweise modernisieren
Strangler Fig statt riskanter Big-Bang-Rewrite

Ein gewachsener PHP-Monolith laesst sich selten in einem grossen Sprung ersetzen. Legacy PHP Code laesst sich aber sehr wohl kontrolliert modernisieren, wenn man Seams identifiziert, Charakterisierungstests schreibt und den alten Code Stueck fuer Stueck mit dem Strangler Fig Pattern umschliesst, statt ihn in einem riskanten Rewrite komplett zu ersetzen.

18 Min. Lesezeit Strangler Fig · Seams · Charakterisierungstests · Rector PHP 7.x bis 8.4 · Legacy-Monolithen

1. Warum der Big-Bang-Rewrite fast immer scheitert

Wer vor einem gewachsenen Stueck Legacy PHP Code steht, denkt fast automatisch an einen kompletten Neuaufbau. Das Argument klingt einleuchtend: der alte Code ist unuebersichtlich, schlecht getestet und schwer erweiterbar, also baut man ihn direkt mit modernem PHP 8.4, sauberer Architektur und vollstaendiger Testabdeckung neu. In der Praxis scheitert dieser Ansatz jedoch ueberraschend haeufig, weil waehrend der Monate oder Jahre dauernden Neuentwicklung das alte System weiterlaeuft, weiter Fehler bekommt und der Abstand zwischen altem und neuem System staendig waechst.

Das zweite Problem ist Wissen, das nur implizit im alten Legacy PHP Code steckt: Sonderfaelle, die vor Jahren fuer einen einzelnen Kunden eingebaut wurden, Workarounds fuer laengst vergessene Bugs in Drittanbieter-Bibliotheken, oder Geschaeftsregeln, die nirgendwo dokumentiert sind ausser im Code selbst. Ein Rewrite-Team, das dieses Wissen nicht kennt, reproduziert unweigerlich Regressionen, die erst nach dem Go-Live auffallen. Schrittweise Modernisierung vermeidet dieses Risiko, weil jede Aenderung klein, ueberpruefbar und sofort produktiv nutzbar ist, statt auf einen einzigen grossen Umschaltmoment zu warten.

Der dritte Grund ist rein wirtschaftlich: ein Big-Bang-Rewrite bindet ueber lange Zeit Entwicklungskapazitaet, ohne dass in dieser Zeit neue Funktionen fuer Kunden entstehen. Schrittweise Modernisierung von Legacy PHP Code liefert dagegen kontinuierlich Wert, weil jeder modernisierte Codeabschnitt sofort von besserer Wartbarkeit profitiert, waehrend das Gesamtsystem weiterhin Umsatz generiert. Die folgenden Abschnitte zeigen konkrete Techniken, mit denen sich diese schrittweise Modernisierung sicher umsetzen laesst.

2. Charakterisierungstests vor dem ersten Refactoring schreiben

Bevor man auch nur eine Zeile Legacy PHP Code aendert, braucht man ein Sicherheitsnetz, das aktuelles Verhalten dokumentiert, nicht gewuenschtes Verhalten. Ein Charakterisierungstest unterscheidet sich von einem klassischen Unit-Test genau darin: er behauptet nicht, was richtig ist, sondern haelt fest, was der Code heute tatsaechlich tut, inklusive aller Bugs und Sonderfaelle. Erst wenn diese Tests gruen sind, kann man mit Refactoring beginnen und nach jeder Aenderung pruefen, ob sich das beobachtbare Verhalten unabsichtlich veraendert hat.

In der Praxis beginnt man mit den am haeufigsten aufgerufenen Funktionen und den kritischsten Geschaeftsprozessen, weil dort das Risiko einer Regression am teuersten waere. Fuer Legacy PHP Code ohne saubere Interfaces bedeutet das oft, zunaechst grobe Ende-zu-Ende-Tests zu schreiben, die eine ganze Anfrage durchlaufen und die resultierende HTTP-Antwort oder den Datenbankzustand pruefen. Erst danach folgen feingranularere Tests fuer einzelne Funktionen, sobald diese isoliert genug sind, um getestet zu werden.


<?php

declare(strict_types=1);

// Characterization test: documents CURRENT behavior of legacy code,
// including quirks that would be bugs in a greenfield project.
final class LegacyPriceCalculatorCharacterizationTest extends PHPUnit\Framework\TestCase
{
    public function testAppliesDiscountBeforeTaxNotAfter(): void
    {
        // NOTE: This is the observed (not necessarily "correct") order.
        // Legacy calculateFinalPrice() applies discount BEFORE tax,
        // which differs from the newer pricing docs. We lock this in
        // as the baseline before touching the function.
        $calculator = new LegacyPriceCalculator();

        $result = $calculator->calculateFinalPrice(
            basePrice: 100.00,
            discountPercent: 10.0,
            taxPercent: 19.0
        );

        // 100 - 10% = 90, then + 19% tax = 107.10
        $this->assertSame(107.10, $result);
    }

    public function testNegativeDiscountIsSilentlyClampedToZero(): void
    {
        // Undocumented quirk found in the legacy code: negative
        // discounts are clamped instead of throwing. We record it
        // so a future refactor does not accidentally "fix" it and
        // break some hidden caller relying on this behavior.
        $calculator = new LegacyPriceCalculator();

        $result = $calculator->calculateFinalPrice(100.00, -5.0, 0.0);

        $this->assertSame(100.00, $result);
    }
}

Wichtig ist, dass Charakterisierungstests keine moralische Bewertung des Legacy PHP Code vornehmen. Ein Test, der eine seltsame Rundungsregel oder eine unerwartete Reihenfolge von Rabatt und Steuer festhaelt, ist kein Fehler im Test, sondern genau sein Zweck. Erst wenn ein Product Owner explizit bestaetigt, dass ein Verhalten tatsaechlich falsch war, aendert man zuerst den Test und danach den Code, niemals umgekehrt gleichzeitig.

3. Das Strangler-Fig-Pattern fuer Legacy PHP Code

Das Strangler-Fig-Pattern, benannt nach dem Wuergefeigenbaum, der einen Wirtsbaum langsam umwaechst und irgendwann komplett ersetzt, ist die zentrale Technik fuer die Modernisierung von Legacy PHP Code. Statt das alte System auf einmal zu ersetzen, legt man eine Routing-Schicht davor, die Anfragen entweder an den alten oder an den neuen Code weiterleitet. Neue Funktionalitaet und modernisierte Bereiche wandern in den neuen Code, waehrend unveraenderter Legacy PHP Code weiterhin bedient wird, bis auch er irgendwann ersetzt ist.

Der entscheidende Vorteil dieses Ansatzes ist, dass zu jedem Zeitpunkt ein funktionierendes Gesamtsystem existiert. Es gibt keinen Tag, an dem alter und neuer Code gleichzeitig fertig sein muessen, weil beide parallel im Produktionsbetrieb koexistieren. Bei einer typischen PHP-Anwendung realisiert man die Routing-Schicht oft ueber den Webserver, ueber eine zentrale Front-Controller-Logik oder ueber ein Feature-Flag-System, das pro Route oder pro Nutzer entscheidet, welcher Codepfad greift.


<?php

declare(strict_types=1);

// Strangler facade: routes requests to legacy or modernized handler
// based on a route whitelist that grows as migration progresses.
final class StranglerRouter
{
    /** @var array<string, bool> */
    private array $migratedRoutes;

    public function __construct(
        private readonly LegacyOrderController $legacyController,
        private readonly ModernOrderController $modernController,
    ) {
        // Only routes listed here are served by the new code path.
        // Everything else still goes through the legacy controller.
        $this->migratedRoutes = [
            'order.create'  => true,
            'order.cancel'  => true,
            'order.refund'  => false, // not migrated yet
        ];
    }

    public function handle(string $routeName, array $request): Response
    {
        if ($this->migratedRoutes[$routeName] ?? false) {
            return $this->modernController->handle($request);
        }

        return $this->legacyController->handle($request);
    }
}

Ein Nebeneffekt des Strangler-Fig-Patterns ist, dass es den Modernisierungsdruck sozial verteilt: jedes Team kann einen kleinen, klar abgegrenzten Bereich uebernehmen, ohne die gesamte Anwendung verstehen zu muessen. Diese Eigenschaft macht die schrittweise Modernisierung von Legacy PHP Code auch in Organisationen mit mehreren Teams und begrenzten Ressourcen praktikabel, weil niemand ein monatelanges Rewrite-Projekt alleine stemmen muss.

4. Seams finden: wo man den Code sicher aufschneiden kann

Ein Seam, ein Begriff aus Michael Feathers Arbeit ueber Legacy PHP Code, ist eine Stelle im Code, an der man das Verhalten aendern kann, ohne den Code an dieser Stelle selbst zu editieren. In PHP sind die haeufigsten Seams Funktionsaufrufe, Methodenaufrufe auf Objekten und Klassenkonstruktion. Ein direkter Aufruf einer globalen Funktion wie mail() oder ein new PDO(...) mitten in Geschaeftslogik ist dagegen kein Seam, weil man ihn nicht ohne Aenderung des umgebenden Codes ersetzen kann.

Der praktische Nutzen von Seams liegt darin, dass man an ihnen Testdoubles einsetzen kann, ohne die eigentliche Logik anzufassen. Ein typisches Vorgehen bei Legacy PHP Code ist, harte Abhaengigkeiten wie Datenbankverbindungen oder Dateisystemzugriffe zunaechst hinter ein Interface zu ziehen, ohne die interne Implementierung zu aendern. Dieser erste Schritt schafft einen Seam, an dem spaeter Tests und schliesslich auch die eigentliche Modernisierung ansetzen koennen.

Ein haeufiger Fehler ist, sofort die "perfekte" Architektur einzufuehren, bevor ueberhaupt ein Seam existiert. Stattdessen sollte man den kleinstmoeglichen Schritt gehen: eine globale Funktion durch einen Methodenaufruf auf einem injizierten Objekt ersetzen, ohne gleichzeitig das Interface, die Implementierung und den Aufrufer neu zu strukturieren. Diese Disziplin, immer nur einen Seam gleichzeitig zu oeffnen, haelt jede einzelne Aenderung an Legacy PHP Code klein genug, um sie in einem einzigen Commit sicher zu ueberpruefen.

5. Legacy-Grenzen mit Facades und Adaptern isolieren

Sobald ein Seam existiert, braucht man eine Struktur, die die Grenze zwischen altem und neuem Code sauber haelt. Eine Facade fasst mehrere zusammenhaengende, aber unuebersichtliche Aufrufe an Legacy PHP Code hinter einer einzigen, klaren Schnittstelle zusammen. Ein Adapter uebersetzt zwischen der alten und der neuen Schnittstelle, sodass neuer Code niemals direkt mit den Eigenheiten des alten Systems in Beruehrung kommt.

Diese Isolation ist entscheidend, weil sie verhindert, dass sich Altlasten wie inkonsistente Rueckgabewerte, globale Zustandsabhaengigkeiten oder fehlende Typisierung in den neuen Code hinein ausbreiten. Jeder neue Code greift ausschliesslich auf den Adapter zu, der intern mit dem chaotischen Legacy PHP Code umgeht und nach aussen eine saubere, typisierte Schnittstelle anbietet.


<?php

declare(strict_types=1);

// Adapter: wraps messy legacy functions behind a clean, typed interface
// so that new code never touches the legacy quirks directly.
interface CustomerRepositoryInterface
{
    public function findById(int $customerId): ?CustomerData;
}

final class LegacyCustomerRepositoryAdapter implements CustomerRepositoryInterface
{
    public function findById(int $customerId): ?CustomerData
    {
        // legacy_get_customer_row() returns an associative array
        // or FALSE on failure — never null, never an object.
        $row = legacy_get_customer_row($customerId);

        if ($row === false) {
            return null;
        }

        // Translate legacy string-typed fields into a proper value object.
        return new CustomerData(
            id: (int) $row['customer_id'],
            email: (string) $row['email_addr'],
            createdAt: new DateTimeImmutable($row['created_ts']),
        );
    }
}

Der Adapter wird bewusst duenn gehalten: er enthaelt keine Geschaeftslogik, sondern ausschliesslich Uebersetzung. Diese Trennung sorgt dafuer, dass Modernisierungsarbeit an Legacy PHP Code messbar bleibt: man kann jederzeit zaehlen, wie viele Aufrufer noch ueber den Adapter auf die alte Implementierung zugreifen, und diese Zahl im Zeitverlauf gegen null treiben.

6. Rector und PHPStan als automatisiertes Sicherheitsnetz

Manuelles Refactoring von grossen Mengen Legacy PHP Code ist fehleranfaellig und langsam. Rector automatisiert mechanische Transformationen wie das Hinzufuegen von Typdeklarationen, das Ersetzen veralteter Funktionsaufrufe oder das Migrieren auf neue Sprachfeatures, basierend auf einem AST statt auf einfachen Textersetzungen. Das macht Rector deutlich sicherer als Suchen-und-Ersetzen, weil es den syntaktischen Kontext jeder Codestelle versteht.

PHPStan ergaenzt Rector, indem es nach jeder automatisierten Transformation prueft, ob neue Typinkonsistenzen entstanden sind. Bei Legacy PHP Code beginnt man dabei fast immer auf einem niedrigen Level wie 1 oder 2, weil hoehere Level sofort hunderte Fehler melden wuerden, die das Team laehmen statt motivieren. Der Level wird dann schrittweise angehoben, sobald die haeufigsten Fehlerklassen abgearbeitet sind.


<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Php80\Rector\FunctionLike\MixedTypeRector;

// rector.php — incremental, low-risk modernization of legacy code.
return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__ . '/src/Legacy',
    ]);

    // Apply changes up to PHP 8.1 sets only — conservative first pass.
    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_81,
    ]);

    // Skip files that are scheduled for a full rewrite anyway,
    // to avoid wasting review time on soon-to-be-deleted code.
    $rectorConfig->skip([
        __DIR__ . '/src/Legacy/DeprecatedModule',
    ]);
};

Ein bewaehrter Ablauf ist, Rector nie direkt gegen den Hauptzweig laufen zu lassen, sondern die Vorschlaege in einem separaten Branch zu pruefen und mit den Charakterisierungstests aus Abschnitt zwei abzusichern. So bleibt die Modernisierung von Legacy PHP Code nachvollziehbar, weil jede automatisierte Aenderung einzeln reviewbar bleibt, statt in einem riesigen, unpruefbaren Commit zu versinken.

7. Feature Flags und Parallelbetrieb waehrend der Migration

Feature Flags erlauben es, neuen und alten Code gleichzeitig im selben Deployment zu betreiben und die Entscheidung, welcher Pfad ausgefuehrt wird, ohne neuen Release zu aendern. Fuer Legacy PHP Code ist das besonders wertvoll bei riskanten Aenderungen wie einer neuen Zahlungsabwicklung oder einer neuen Preisberechnung, weil man den neuen Pfad zunaechst nur fuer einen kleinen Prozentsatz an Traffic oder fuer interne Testnutzer aktivieren kann.

Eine fortgeschrittene Variante ist der Schatten-Modus, bei dem sowohl der alte als auch der neue Code ausgefuehrt werden, aber nur das Ergebnis des alten Codes tatsaechlich an den Nutzer ausgeliefert wird. Die Ergebnisse beider Pfade werden verglichen und Abweichungen geloggt, ohne dass ein Nutzer jemals ein fehlerhaftes Ergebnis aus dem neuen, noch nicht vollstaendig vertrauten Code sieht. Dieser Ansatz liefert bei der Modernisierung von Legacy PHP Code harte Daten darueber, ob der neue Code tatsaechlich aequivalent ist, statt sich auf Testabdeckung alleine zu verlassen.


<?php

declare(strict_types=1);

// Shadow mode: run both implementations, serve the legacy result,
// but log discrepancies so we can trust the new path before switching.
final class ShadowModePriceCalculator
{
    public function __construct(
        private readonly LegacyPriceCalculator $legacy,
        private readonly ModernPriceCalculator $modern,
        private readonly LoggerInterface $logger,
    ) {
    }

    public function calculate(Order $order): float
    {
        $legacyResult = $this->legacy->calculateFinalPrice(
            $order->basePrice,
            $order->discountPercent,
            $order->taxPercent
        );

        try {
            $modernResult = $this->modern->calculate($order);

            if (abs($legacyResult - $modernResult) > 0.01) {
                $this->logger->warning('Price mismatch detected', [
                    'order_id' => $order->id,
                    'legacy'   => $legacyResult,
                    'modern'   => $modernResult,
                ]);
            }
        } catch (Throwable $e) {
            $this->logger->error('Modern calculator threw during shadow run', [
                'order_id' => $order->id,
                'exception' => $e->getMessage(),
            ]);
        }

        // Legacy result is always returned to the customer for now.
        return $legacyResult;
    }
}

8. Team-Organisation: Boy-Scout-Rule und feste Modernisierungs-Slots

Selbst die beste Technik scheitert, wenn Modernisierung von Legacy PHP Code organisatorisch keinen Platz hat. Die Boy-Scout-Rule, also die Regel, jeden Codeabschnitt etwas sauberer zu hinterlassen als man ihn vorgefunden hat, funktioniert gut fuer kleine, beilaeufige Verbesserungen, reicht aber nicht fuer strukturelle Modernisierung, die mehrere Tage geplante Arbeit braucht.

Bewaehrt hat sich, feste Kapazitaet, etwa zehn bis zwanzig Prozent jedes Sprints, explizit fuer Modernisierungsarbeit an Legacy PHP Code zu reservieren, statt sie implizit gegen Feature-Arbeit konkurrieren zu lassen. Ohne diese feste Reservierung verliert Modernisierung fast immer gegen Termindruck, weil kurzfristig sichtbare Feature-Arbeit im Projektmanagement meist hoehere Prioritaet bekommt als unsichtbare interne Qualitaet.

Wichtig ist zudem, Fortschritt sichtbar zu machen: eine einfache Kennzahl wie der Anteil an Code, der bereits durch PHPStan Level 5 oder hoeher laeuft, oder die Anzahl der Aufrufer, die noch ueber Legacy-Adapter gehen, macht den Fortschritt der Modernisierung von Legacy PHP Code fuer das gesamte Team und das Management greifbar und rechtfertigt die investierte Zeit.

9. Strangler Fig, Big Bang und Freeze-and-Replace im Vergleich

Neben dem Strangler-Fig-Pattern gibt es weitere Strategien fuer den Umgang mit Legacy PHP Code, die je nach Projektgroesse und Risikotoleranz unterschiedlich gut passen. Die folgende Tabelle stellt die drei gaengigsten Ansaetze gegenueber.

Strategie Risiko Time-to-Value Wann geeignet
Big-Bang-Rewrite Sehr hoch Erst nach Monaten/Jahren Sehr kleine Anwendungen, klar begrenzter Scope
Strangler Fig Gering, inkrementell steuerbar Sofort, pro Modul Grosse, dauerhaft laufende Systeme
Freeze-and-Replace Mittel Erst nach Ersatzsystem live Ablaufende Systeme mit fixem Enddatum
Boy-Scout-Rule allein Gering, aber langsamer Fortschritt Kontinuierlich, sehr langsam Ergaenzend zu Strangler Fig, nicht als Ersatz

In der Praxis kombiniert man diese Strategien haeufig: der Kern des Systems wird per Strangler Fig modernisiert, wirklich veraltete Randmodule ohne Zukunft werden per Freeze-and-Replace ausgetauscht, sobald ein fertiges Ersatzmodul existiert, und die Boy-Scout-Rule sorgt fuer kontinuierliche kleine Verbesserungen zwischen den groesseren Modernisierungsschritten an Legacy PHP Code.

Mironsoft

PHP-Modernisierung, Legacy-Refactoring und Magento-Entwicklung

Ihr Legacy PHP Code bremst neue Features aus?

Wir analysieren bestehenden PHP-Code, identifizieren Seams und Modernisierungspfade und begleiten die schrittweise Migration mit Charakterisierungstests, Rector und dem Strangler-Fig-Pattern, ohne dass Ihr Betrieb stillsteht.

Legacy-Audit

Seams identifizieren, Risikobereiche priorisieren, Modernisierungsplan erstellen

Refactoring

Charakterisierungstests, Facades, Adapter und automatisierte Rector-Migrationen

Begleitung

Feature Flags, Shadow-Mode-Vergleiche und Team-Coaching fuer nachhaltige Qualitaet

10. Zusammenfassung

Die Modernisierung von Legacy PHP Code gelingt selten durch einen einzigen grossen Sprung, sondern durch eine Kette kleiner, abgesicherter Schritte. Charakterisierungstests dokumentieren das bestehende Verhalten, bevor irgendetwas geaendert wird. Seams schaffen die Stellen, an denen man den Code sicher aufschneiden kann, und Facades sowie Adapter isolieren die Grenze zwischen alt und neu, damit sich Altlasten nicht in frischen Code hinein ausbreiten.

Das Strangler-Fig-Pattern haelt zu jedem Zeitpunkt ein funktionsfaehiges Gesamtsystem aufrecht, waehrend Rector und PHPStan mechanische Transformationen automatisieren und Feature Flags sowie Shadow-Mode-Vergleiche das Risiko jeder einzelnen Aenderung minimieren. Ohne feste organisatorische Reservierung von Kapazitaet bleibt jede Technik jedoch Theorie, denn Modernisierung von Legacy PHP Code konkurriert immer mit sichtbarer Feature-Arbeit und braucht deshalb einen expliziten, verteidigten Platz im Sprint-Plan.

Legacy PHP Code modernisieren — Das Wichtigste auf einen Blick

Sicherheitsnetz zuerst

Charakterisierungstests dokumentieren bestehendes Verhalten, bevor ein einziges Refactoring stattfindet.

Strangler Fig statt Big Bang

Eine Routing-Schicht leitet Anfragen an alten oder neuen Code, das Gesamtsystem bleibt jederzeit lauffaehig.

Seams, Facades, Adapter

Legacy-Grenzen sauber isolieren, damit neue Codebereiche nie direkt mit alten Eigenheiten in Beruehrung kommen.

Automatisierung und Organisation

Rector und PHPStan automatisieren mechanische Schritte, feste Sprint-Kapazitaet sichert langfristigen Fortschritt.

11. FAQ: Legacy PHP Code schrittweise modernisieren

1Warum scheitert ein Big-Bang-Rewrite so oft?
Waehrend der Neuentwicklung laeuft das alte System weiter und der Abstand waechst. Implizites Wissen geht verloren, was zu Regressionen nach dem Go-Live fuehrt.
2Was ist ein Charakterisierungstest?
Ein Test, der das aktuelle Verhalten dokumentiert, inklusive Bugs, statt zu bewerten was richtig waere. Das Sicherheitsnetz vor jedem Refactoring.
3Was ist ein Seam?
Eine Stelle, an der man Verhalten aendern kann ohne den Code dort direkt zu editieren, zum Beispiel ein Methodenaufruf statt einer globalen Funktion.
4Wie funktioniert Strangler Fig?
Eine Routing-Schicht entscheidet pro Anfrage ueber alten oder neuen Codepfad. Der alte Code wird schrittweise abgeloest, das Gesamtsystem bleibt lauffaehig.
5Wozu Facade oder Adapter?
Sie isolieren die Grenze zwischen alt und neu, damit neuer Code nie direkt mit Eigenheiten des Legacy-Codes in Beruehrung kommt.
6Welches PHPStan-Level am Anfang?
Meist Level 1 oder 2. Hoehere Level melden bei unbehandeltem Legacy-Code sofort hunderte Fehler, der Level steigt schrittweise.
7Feature Flag vs. Shadow Mode?
Feature Flag waehlt den ausgefuehrten Pfad. Shadow Mode laesst beide parallel laufen, liefert aber nur das alte Ergebnis aus und loggt Abweichungen.
8Reicht die Boy-Scout-Rule allein?
Nein, sie eignet sich fuer kleine Verbesserungen, nicht fuer strukturelle Modernisierung. Dafuer braucht es feste Sprint-Kapazitaet.
9Wie hilft Rector konkret?
Rector automatisiert AST-basierte Codetransformationen, was deutlich sicherer ist als manuelles Suchen und Ersetzen im Text.
10Fortschritt fuers Management sichtbar machen?
Mit Kennzahlen wie dem Anteil an Code auf einem bestimmten PHPStan-Level oder der Zahl verbleibender Legacy-Adapter-Aufrufer.