Prozedurales PHP zu objektorientiertem Code migrieren
AI generated
<?php
8.4
PHP · Legacy · OOP-Migration · Refactoring
Prozedurales PHP zu objektorientiertem Code migrieren
Von Funktionssammlungen zu wartbaren Klassen

Viele gewachsene PHP-Projekte bestehen aus hunderten freien Funktionen, die über include-Dateien lose zusammengehalten werden. Dieser Artikel zeigt, wie man solche prozeduralen Codebasen schrittweise in objektorientiertes PHP überführt, mit erkennbaren Extraktionsmustern statt eines riskanten kompletten Neuschreibens.

19 Min. Lesezeit Funktionsgruppen · Klassen extrahieren · Zustand kapseln PHP 8.x · Legacy-Migration

1. Warum prozedurales PHP an Grenzen stößt

Prozedurales PHP ist nicht per se schlecht, für kleine Skripte mit klarem Ablauf ist es oft die schnellste Lösung. Das Problem entsteht, wenn ein solches Skript über Jahre wächst und Funktionen anfangen, sich gegenseitig aufzurufen, gemeinsamen Zustand über globale Variablen zu teilen und in include-Dateien verstreut zu liegen, ohne dass eine klare Struktur erkennbar bleibt. Genau an diesem Punkt beginnt die Migration zu objektorientiertem PHP sich auszuzahlen, weil Klassen Struktur, Kapselung und Namensräume mitbringen, die reine Funktionen nicht bieten.

Der typische Schmerzpunkt zeigt sich beim Onboarding neuer Entwickler: In einer prozeduralen Codebasis mit hunderten Funktionen in einer flachen Namensraumhierarchie ist unklar, welche Funktion zu welchem fachlichen Bereich gehört und welche Daten sie tatsächlich benötigt. Eine Funktion wie berechne_versandkosten($bestellung) greift in der Praxis oft still auf ein Dutzend globaler Variablen zu, die nirgends in der Signatur auftauchen. Diese Unsichtbarkeit von Abhängigkeiten ist der eigentliche Grund, warum prozedurales PHP in großen Projekten unwartbar wird, nicht die Syntax selbst.

Die Migration zu objektorientiertem Code löst dieses Problem nicht magisch, sondern macht es sichtbar und bearbeitbar: Eine Klasse mit typisierten Konstruktor-Parametern zwingt dazu, jede Abhängigkeit explizit zu benennen. Der Rest dieses Artikels zeigt, wie man von einer Sammlung freier Funktionen zu einer Menge kohärenter Klassen kommt, ohne die Anwendung während der Migration lahmzulegen.

2. Funktionsgruppen als versteckte Klassen erkennen

Der erste Schritt jeder Migration ist keine Codeänderung, sondern eine Analyse: Welche Funktionen in der bestehenden Codebasis operieren wiederholt auf denselben Daten? Eine Gruppe von Funktionen, die alle ein $bestellung-Array als ersten Parameter entgegennehmen und verändern, ist in Wahrheit eine Klasse, die noch nicht als solche erkannt wurde. Diese Beobachtung, oft als "Datenklumpen" (Data Clumps) bezeichnet, ist eines der zuverlässigsten Signale für eine sinnvolle Klassenextraktion.

Praktisch findet man solche Gruppen, indem man Funktionsnamen nach gemeinsamen Präfixen durchsucht, etwa bestellung_berechnen, bestellung_validieren, bestellung_stornieren. Dieses Namensmuster ist meist kein Zufall, sondern eine informelle, in der Sprache selbst nicht abgebildete Klasse. Ein gutes Werkzeug für diese Analyse ist ein einfaches Skript, das per Regex alle Funktionsdefinitionen einer Datei extrahiert und nach gemeinsamen Präfixen gruppiert, um einen ersten Überblick über mögliche Klassen zu bekommen, bevor überhaupt eine Zeile Code geändert wird.


<?php
// Legacy procedural functions — all operate on the same $order array
// This is a hidden class that has not been recognized as such yet

function bestellung_gesamtsumme(array $bestellung): float
{
    $summe = 0.0;
    foreach ($bestellung['positionen'] as $position) {
        $summe += $position['preis'] * $position['menge'];
    }
    return $summe;
}

function bestellung_ist_versandfaehig(array $bestellung): bool
{
    return $bestellung['status'] === 'bezahlt'
        && !empty($bestellung['lieferadresse']);
}

function bestellung_versandkosten(array $bestellung): float
{
    $gewicht = array_sum(array_column($bestellung['positionen'], 'gewicht'));
    return $gewicht > 5.0 ? 9.90 : 4.90;
}

// Somewhere else in the codebase, the same array is passed around
// with no guarantee about its shape or invariants
$bestellung = ['positionen' => [], 'status' => 'neu', 'lieferadresse' => null];
$summe = bestellung_gesamtsumme($bestellung);

Alle drei Funktionen operieren auf demselben Array, ohne dass eine gemeinsame Struktur oder Validierung existiert. Jede Funktion muss selbst prüfen, ob die erwarteten Schlüssel überhaupt vorhanden sind, was zu wiederholtem, defensivem Code führt. Genau diese Wiederholung ist das Signal, dass $bestellung eine echte Klasse werden sollte.

3. Die erste Klasse extrahieren: ein Praxisbeispiel

Die Extraktion beginnt mit der Definition einer Klasse, die die bisherigen Array-Schlüssel als typisierte Eigenschaften abbildet, und Methoden, die den Inhalt der vorherigen Funktionen übernehmen. Wichtig ist, die Methode zunächst so nah wie möglich am ursprünglichen Verhalten zu halten, statt gleichzeitig Fachlogik zu verbessern. Verhalten und Struktur getrennt zu ändern ist der Kern von sicherem Refactoring, jede Vermischung von beidem erhöht das Risiko, unbemerkt einen Bug einzuführen.

Im folgenden Beispiel wird aus den drei freien Funktionen eine Klasse Order, deren Konstruktor die vorher lose zusammengehaltenen Daten zusammenfasst und deren Methoden die identische Fachlogik wie zuvor ausführen, nur jetzt gekapselt und mit garantierter Datenstruktur.


<?php

declare(strict_types=1);

final class OrderLine
{
    public function __construct(
        public readonly float $price,
        public readonly int $quantity,
        public readonly float $weight,
    ) {
    }
}

// Extracted class replaces the loosely structured $bestellung array
final class Order
{
    /** @param OrderLine[] $lines */
    public function __construct(
        private readonly array $lines,
        private readonly string $status,
        private readonly ?string $shippingAddress,
    ) {
    }

    public function total(): float
    {
        $sum = 0.0;
        foreach ($this->lines as $line) {
            $sum += $line->price * $line->quantity;
        }
        return $sum;
    }

    public function isReadyToShip(): bool
    {
        return $this->status === 'paid' && $this->shippingAddress !== null;
    }

    public function shippingCost(): float
    {
        $weight = array_sum(array_map(
            static fn (OrderLine $line): float => $line->weight,
            $this->lines
        ));
        return $weight > 5.0 ? 9.90 : 4.90;
    }
}

$order = new Order(
    lines: [new OrderLine(price: 19.99, quantity: 2, weight: 0.5)],
    status: 'paid',
    shippingAddress: 'Musterstraße 1, 12345 Berlin',
);
echo $order->total();

Der entscheidende Unterschied zur prozeduralen Variante liegt nicht in der Funktionalität, sondern in der Garantie: Ein Order-Objekt kann nicht in einem inkonsistenten Zwischenzustand existieren, weil der Konstruktor alle notwendigen Daten verlangt. Bei der Array-Variante konnte jeder Aufrufer versehentlich einen Schlüssel vergessen, ohne dass PHP dies zur Entwicklungszeit bemerkt hätte.

4. Impliziten Zustand in Eigenschaften kapseln

Neben Funktionsgruppen, die auf Arrays operieren, findet man in prozeduralem Code häufig impliziten Zustand über statische Funktionsvariablen oder Dateien, die als primitive Datenbank dienen. Dieser Zustand ist besonders tückisch, weil er zwischen Aufrufen bestehen bleibt, ohne dass das im Funktionsnamen sichtbar wird. Eine Funktion naechste_bestellnummer(), die intern eine statische Variable hochzählt, verhält sich bei jedem Aufruf anders, obwohl die Signatur keine Parameter zeigt.

Die objektorientierte Entsprechung ist eine Klasse mit einer privaten Eigenschaft, die den Zustand explizit hält, und deren Instanz gezielt dort erzeugt wird, wo der Zustand benötigt wird. Dadurch wird aus einem unsichtbaren, global geteilten Zustand ein lokal kontrollierter, dessen Lebensdauer über die Objektinstanz sichtbar ist. Genau dieser Schritt, impliziten in expliziten Zustand zu überführen, ist oft wertvoller als die reine Umbenennung von Funktionen in Methoden, weil er die eigentliche Fehlerquelle beseitigt.


<?php
// Legacy: implicit state hidden inside a static function variable
function naechste_bestellnummer(): int
{
    static $counter = 1000;
    return $counter++;
}

declare(strict_types=1);

// Object-oriented equivalent: state is explicit, lifetime is visible
final class OrderNumberGenerator
{
    public function __construct(
        private int $counter = 1000,
    ) {
    }

    public function next(): int
    {
        return $this->counter++;
    }
}

// Two independent generators, no shared hidden state between them
$generatorA = new OrderNumberGenerator();
$generatorB = new OrderNumberGenerator(counter: 5000);
echo $generatorA->next(); // 1000
echo $generatorB->next(); // 5000

5. Assoziative Arrays durch Objekte ersetzen

Assoziative Arrays sind in prozeduralem PHP das universelle Werkzeug für strukturierte Daten, bringen aber keinerlei Garantien mit: Ein Tippfehler im Schlüssel wird erst zur Laufzeit sichtbar, oft als stille null-Rückgabe statt eines Fehlers. Die Migration zu Objekten löst dieses Problem strukturell, weil PHPStan oder Psalm bereits zur Entwicklungszeit erkennen, wenn auf eine nicht existierende Eigenschaft zugegriffen wird, während ein Tippfehler in einem Array-Schlüssel unbemerkt bleibt.

Ein bewährter Zwischenschritt bei größeren Migrationen ist, zunächst nur die am häufigsten verwendeten Arrays in Value Objects umzuwandeln, während seltener genutzte Strukturen vorerst Arrays bleiben. Diese Priorisierung nach Nutzungshäufigkeit sorgt dafür, dass der größte Teil des Typsicherheitsgewinns mit dem geringsten Migrationsaufwand erreicht wird, statt alle Arrays gleichzeitig und mit gleicher Priorität umzubauen.

6. Abwärtskompatible Fassaden für alte Aufrufer

Eine Migration, die alle Aufrufer einer Funktion gleichzeitig auf die neue Klasse umstellen muss, ist in großen Projekten selten praktikabel. Der bewährte Ansatz ist, die ursprüngliche Funktion als dünne Fassade bestehen zu lassen, die intern die neue Klasse instanziiert und deren Methode aufruft. Dadurch funktioniert alter Code unverändert weiter, während neuer Code bereits direkt mit der Klasse arbeitet.


<?php

declare(strict_types=1);

// Backward-compatible facade: old callers keep working unchanged
function bestellung_gesamtsumme(array $bestellung): float
{
    trigger_error(
        'bestellung_gesamtsumme() is deprecated, use Order::total() instead',
        E_USER_DEPRECATED
    );

    $lines = array_map(
        static fn (array $item): OrderLine => new OrderLine(
            price: (float) $item['preis'],
            quantity: (int) $item['menge'],
            weight: (float) ($item['gewicht'] ?? 0.0),
        ),
        $bestellung['positionen'] ?? []
    );

    $order = new Order(
        lines: $lines,
        status: $bestellung['status'] ?? 'neu',
        shippingAddress: $bestellung['lieferadresse'] ?? null,
    );

    return $order->total();
}

Der E_USER_DEPRECATED-Hinweis macht sichtbar, wie oft die alte Funktion noch aufgerufen wird, ohne den Betrieb zu unterbrechen. In Kombination mit einem Log-Handler, der solche Meldungen zentral sammelt, entsteht eine ehrliche, messbare Kennzahl für den Migrationsfortschritt, ganz ohne aufwendige manuelle Codesuche.

7. Die richtige Reihenfolge: Bottom-up statt Top-down

Ein häufiger Fehler bei der Migration ist, mit der größten, zentralsten Funktion zu beginnen, weil sie den größten wahrgenommenen Nutzen verspricht. In der Praxis ist genau das Gegenteil sinnvoller: Man beginnt mit kleinen, blattartigen Funktionen ohne Abhängigkeiten zu anderen Funktionsgruppen, extrahiert diese zuerst in Klassen, und arbeitet sich dann schrittweise zu den zentraleren, stärker vernetzten Funktionen vor.

Dieser Bottom-up-Ansatz hat einen praktischen Grund: Kleine, isolierte Funktionen lassen sich leicht mit Characterization Tests absichern, bevor sie umgebaut werden, während zentrale Funktionen mit vielen Abhängigkeiten ein deutlich höheres Risiko für unbeabsichtigte Verhaltensänderungen tragen. Wer zuerst die einfachen Fälle migriert, sammelt zudem Erfahrung mit dem eigenen Extraktionsprozess, bevor die komplexeren, risikoreicheren Teile der Codebasis an der Reihe sind.

8. Characterization Tests vor der Migration schreiben

Bevor eine Funktion in eine Klasse überführt wird, sollte ein Test existieren, der das aktuelle, tatsächliche Verhalten der Funktion dokumentiert, unabhängig davon, ob dieses Verhalten fachlich korrekt ist. Solche Characterization Tests sind kein Ersatz für eine vollständige Testabdeckung, sie erfüllen einen anderen Zweck: Sie garantieren, dass die Migration das Verhalten nicht unbeabsichtigt verändert, selbst wenn das ursprüngliche Verhalten Kanten oder Sonderfälle enthält, die niemand mehr bewusst kennt.

In der Praxis reicht dafür oft ein einfacher Test, der die alte Funktion mit repräsentativen Eingaben aufruft und die Ausgabe protokolliert, ohne eine explizite Erwartung zu formulieren, sondern das aktuelle Ergebnis als Golden Master festzuhalten. Nach der Extraktion in die Klasse läuft derselbe Test gegen die neue Methode, und jede Abweichung vom Golden Master zeigt sofort eine unbeabsichtigte Verhaltensänderung, bevor sie in Produktion sichtbar wird.


<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

// Characterization test: documents current behavior, not required
// correctness — captures the golden master before extraction
final class OrderTotalCharacterizationTest extends TestCase
{
    public function testGoldenMasterBeforeExtraction(): void
    {
        $order = ['lines' => [
            ['preis' => 19.99, 'menge' => 2, 'gewicht' => 0.5],
        ], 'status' => 'neu', 'lieferadresse' => null];

        // Golden master value observed from the legacy function,
        // not derived from a spec — just what it currently returns
        self::assertSame(39.98, bestellung_gesamtsumme($order));
    }

    public function testGoldenMasterAfterExtraction(): void
    {
        // Same input, same golden master, now against the new class
        $lines = [new OrderLine(price: 19.99, quantity: 2, weight: 0.5)];
        $order = new Order(lines: $lines, status: 'neu', shippingAddress: null);

        self::assertSame(39.98, $order->total());
    }
}

9. Prozedural vs. objektorientiert im direkten Vergleich

Die folgende Tabelle stellt die wichtigsten strukturellen Unterschiede zwischen prozeduralem und objektorientiertem PHP anhand konkreter Kriterien gegenüber, die bei der Migrationsentscheidung eine Rolle spielen.

Kriterium Prozedural Objektorientiert
Datenkonsistenz Array-Schlüssel können fehlen, keine Garantie Konstruktor erzwingt vollständige Daten
Statische Analyse Tippfehler in Array-Schlüsseln unentdeckt PHPStan erkennt falsche Eigenschaftszugriffe
Namensraum Alle Funktionen im globalen Namensraum Klassen mit PSR-4-Namespaces gruppiert
Testbarkeit Globale Abhängigkeiten schwer austauschbar Konstruktor-Injection ermöglicht Mocks
Erweiterbarkeit Neue Fälle erfordern if/switch-Ketten Neue Klassen über Interfaces anschließbar

Die Tabelle zeigt keinen Freifahrtschein für pauschale Umbauten: Kleine, einmalige Skripte profitieren selten von einer vollständigen Klassenstruktur. Für gewachsene, langlebige Anwendungen mit mehreren Entwicklern überwiegen die Vorteile objektorientierter Strukturen jedoch deutlich, insbesondere bei Datenkonsistenz und Testbarkeit.

Mironsoft

PHP-Legacy-Modernisierung und Magento-Entwicklung

Prozedurales PHP endlich in wartbare Klassen überführen?

Wir identifizieren versteckte Funktionsgruppen in eurer Codebasis, extrahieren Klassen schrittweise und sichern jeden Migrationsschritt mit Characterization Tests ab.

Struktur-Analyse

Funktionsgruppen und versteckte Klassen im Bestandscode identifizieren

Schrittweise Extraktion

Klassen bottom-up extrahieren, mit abwärtskompatiblen Fassaden

Absicherung

Characterization Tests vor jeder Migrationsstufe schreiben

10. Zusammenfassung

Die Migration von prozeduralem zu objektorientiertem PHP ist keine reine Syntaxübung, sondern eine strukturelle Verbesserung: Klassen machen implizite Abhängigkeiten sichtbar, erzwingen konsistente Daten über Konstruktoren und ermöglichen statische Analyse, die bei Arrays und freien Funktionen nicht möglich ist. Der Ausgangspunkt jeder Migration ist die Analyse bestehender Funktionsgruppen, die auf denselben Daten operieren und in Wahrheit bereits eine Klasse bilden, ohne dass diese Klasse je benannt wurde.

Der sichere Weg führt über Bottom-up-Extraktion, beginnend bei kleinen, isolierten Funktionen, abgesichert durch Characterization Tests, die das bestehende Verhalten dokumentieren, bevor es umgebaut wird. Abwärtskompatible Fassaden mit E_USER_DEPRECATED-Hinweisen ermöglichen, alten und neuen Code parallel zu betreiben, bis alle Aufrufer migriert sind. Am Ende steht eine Codebasis, in der objektorientiertes PHP nicht aufgezwungen wirkt, sondern die tatsächliche fachliche Struktur der Anwendung sichtbar macht.

Prozedural zu objektorientiert migrieren — Das Wichtigste auf einen Blick

Analyse

Funktionsgruppen mit gemeinsamem Präfix oder gemeinsamen Datenparametern sind versteckte Klassen.

Extraktion

Konstruktor erzwingt vollständige Daten, Methoden übernehmen die Fachlogik unverändert.

Reihenfolge

Bottom-up: kleine, isolierte Funktionen zuerst, zentrale, vernetzte Funktionen zuletzt.

Absicherung

Characterization Tests vor jeder Extraktion, abwärtskompatible Fassaden für alte Aufrufer.

11. FAQ: Prozedural zu objektorientiert migrieren

1Ist prozedurales PHP grundsätzlich schlecht?
Nein. Für kleine, kurzlebige Skripte ist es oft die passendste Lösung. Probleme entstehen erst mit wachsender Größe und geteiltem Zustand.
2Wie erkenne ich versteckte Klassen?
Funktionen mit gemeinsamem Datenparameter oder Namenspräfix operieren meist auf einer fachlichen Einheit, die extrahiert werden sollte.
3Müssen alle Aufrufer gleichzeitig migrieren?
Nein, eine abwärtskompatible Fassade erlaubt schrittweise, unabhängige Migration der Aufrufer.
4Warum Bottom-up statt Top-down?
Kleine, isolierte Funktionen sind risikoärmer und lassen erste Erfahrung mit dem Prozess sammeln, bevor komplexere Teile folgen.
5Was ist ein Characterization Test?
Ein Test, der aktuelles Verhalten dokumentiert, unabhängig von fachlicher Korrektheit, um unbeabsichtigte Änderungen zu erkennen.
6Sollte ich alle Arrays sofort ersetzen?
Nein, zuerst die am häufigsten verwendeten Arrays priorisieren, seltene Strukturen können vorerst bestehen bleiben.
7Wie messe ich den Fortschritt?
E_USER_DEPRECATED in Fassaden-Funktionen, zentral geloggt, liefert eine sinkende Kennzahl verbleibender Aufrufer.
8Bringt OOP automatisch mehr Performance?
Nein, der Gewinn liegt bei Wartbarkeit und Typsicherheit, nicht bei Performance.
9Was tue ich mit Funktionen ohne Datenklasse?
Zustandslose Hilfsfunktionen als statische Methoden in einer Utility-Klasse belassen, das reicht meist aus.
10Wie viel Zeit sollte man einplanen?
Meist eine kontinuierliche Migration über Monate, begleitend zur Feature-Entwicklung, statt eines abgetrennten Umbauprojekts.