Alle Artikel
12. September 2026
8 mins

Laravel-Upgrades: wo die Fallen anders liegen

Von Tim Rutte, Cloud & Software ArchitectThemaLegacy & Modernisierung

Eine weiße Trittleiter, deren mittlere Stufe blau ist und aus dem Abstand fällt.

Wer ein Symfony-Upgrade hinter sich hat und danach ein Laravel-Projekt anhebt, macht eine unangenehme Erfahrung: Die Werkzeuge, auf die man sich gewöhnt hat, greifen nicht. Es gibt kein Log, das die Arbeit auflistet, und ein sauberer Testlauf sagt weniger über den Zustand aus, als man annimmt.

Das ist kein Mangel an Sorgfalt bei Laravel, sondern eine Folge davon, wie das Framework gebaut ist. Vieles, was in Symfony eine ausdrückliche Konfiguration ist, ist in Laravel eine Konvention, und Konventionen ändern sich ohne einen Aufruf, der sich als veraltet melden könnte.

Dieser Artikel beschreibt vier Stellen, an denen Laravel-Upgrades ihre eigenen Fallen haben, und wie man sie vor dem Sprung findet statt danach.

Warum die gewohnten Werkzeuge nicht greifen

Symfony verspricht Stabilität der öffentlichen Schnittstelle innerhalb einer Hauptversion und markiert alles, was verschwindet, vorher als veraltet. Daraus entsteht eine Liste, die man abarbeitet.

Laravel arbeitet anders. Es gibt eine Upgrade-Anleitung je Hauptversion, die sehr ordentlich gepflegt ist, und es gibt Deprecations, aber sie decken einen kleineren Teil ab. Der Grund liegt in drei Eigenschaften des Frameworks, die im Alltag Vorteile sind und beim Upgrade zu Arbeit werden.

Verhalten steckt in Konventionen. Wie ein Modellname auf einen Tabellennamen abgebildet wird, wie ein Wert beim Lesen umgewandelt wird, in welcher Reihenfolge etwas ausgeführt wird: Das ist Verhalten ohne Aufruf. Ändert es sich, meldet sich nichts, und die Auswirkung ist ein anderer Wert, kein Fehler.

Vieles wird zur Laufzeit aufgelöst. Fassaden, dynamische Eigenschaften, magische Methoden. Eine statische Analyse sieht davon weniger als in einem Symfony-Projekt, und dieselbe Analyse ist es, die bei Symfony einen Großteil der Fundstellen liefert.

Der Rahmen des Projekts ist Teil der Anwendung. Konfigurationsdateien, die Startdatei, die Ausnahmebehandlung: Das alles liegt im Projekt, wurde beim Anlegen einmal kopiert und läuft seitdem still auseinander von dem, was aktuell wäre.

Die Folge für die Planung: Ein Laravel-Upgrade ist weniger Listenarbeit und mehr Vergleich. Das ist nicht schlimmer, es ist anders, und wer es wie ein Symfony-Upgrade plant, liegt daneben.

Falle 1: Eloquent tut etwas anderes, ohne es zu sagen

Die teuerste Fundstelle liegt fast immer im Datenzugriff, und zwar nicht als Fehler, sondern als anderer Wert.

Drei Muster tauchen regelmäßig auf.

Umwandlungen beim Lesen. Ein Feld, das als float deklariert ist, ein Datumsformat, das in einer Version anders ausgegeben wird als in der nächsten, ein boolean, der aus einer CHAR(1)-Spalte kommt. Das Ergebnis wandert in eine Ausgabe oder eine Schnittstelle, und dort fällt es Wochen später auf.

Ereignisse beim Speichern. Ob ein Massen-Update die Modell-Ereignisse auslöst, ob ein Ereignis in einer Warteschlange landet, ob eine Beziehung beim Speichern mitgespeichert wird: Diese Details haben sich über die Versionen mehrfach geändert. Wo Geschäftslogik an einem Ereignis hängt, läuft sie danach doppelt oder gar nicht.

Beziehungen und ihre Ladezeitpunkte. Änderungen daran, wann eine Beziehung nachgeladen wird, erzeugen keinen Fehler, sondern eine andere Anzahl Abfragen. Die Anwendung funktioniert und wird langsamer, und der Zusammenhang zum Upgrade ist nach zwei Wochen nicht mehr sichtbar.

Der Schutz dagegen ist ein Golden Master, und zwar auf der Ebene, auf der es zählt: nicht auf einzelnen Methoden, sondern auf den Ausgaben, die das Geschäft sieht.

// Vor dem Upgrade laufen lassen, Ergebnis einchecken.
// Nach dem Upgrade erneut: jede Abweichung ist ein Fund.
public function testKundenNutzlastUnveraendert(): void
{
    $kunden = Kunde::with(['vertraege', 'adressen'])
        ->whereIn('id', [17, 402, 1337, 88231])   // bewusst Sonderfaelle
        ->get()
        ->map(fn (Kunde $k) => $k->toArray())
        ->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

    $this->assertMatchesJsonSnapshot($kunden);
}

Die vier Bezeichner im Beispiel sind der Punkt: Datensätze mit Sonderfällen, nicht die ersten vier aus der Tabelle. Ein Kunde ohne Adresse, einer mit gekündigtem Vertrag, einer mit Umlauten im Namen, einer aus der Zeit vor einer Datenmigration.

Der Vergleich lohnt sich: Bei Symfony liegt dieselbe Arbeit an einer anderen Stelle, nämlich im Deprecation-Log. Symfony-Deprecations abarbeiten, ohne im Log zu ertrinken.

Falle 2: das Paket-Ökosystem

Laravel hat ein großes Ökosystem, und ein erheblicher Teil davon ist an Hauptversionen gebunden: Ein Paket unterstützt Laravel 10 und 11, nicht aber 12, und erscheint drei Monate später oder nie.

Daraus folgt der wichtigste Planungsschritt, und er kommt vor jeder Schätzung:

# Wer blockiert die Zielversion? Eine Zeile, eine vollstaendige Antwort.
composer why-not laravel/framework 12.0

# Und die aufgegebenen Pakete separat, sie sind der teure Teil
composer show --direct --format=json \
  | php -r '$d=json_decode(file_get_contents("php://stdin"),true);
    foreach($d["installed"] as $p) if(!empty($p["abandoned"]))
      echo $p["name"], PHP_EOL;'

Die Liste sortiert sich in drei Gruppen, und die Antworten unterscheiden sich stark. Ein Paket, das nachzieht, kostet Wartezeit. Ein Paket mit anerkanntem Nachfolger kostet einen Umbau an wenigen Stellen. Ein aufgegebenes Paket ohne Nachfolger kostet Eigenbau, und das ist der Posten, der die Laufzeit des Vorhabens bestimmt.

Eine Beobachtung aus der Praxis, die Zeit spart: Pakete, die nur Bequemlichkeit liefern, also ein paar Hilfsmethoden oder eine hübschere Fassade, werden beim Upgrade eher entfernt als ersetzt. Der Aufwand, sie zu ersetzen, ist meist größer als der, ohne sie auszukommen.

Falle 3: die Oberflächenkette

Die Kette aus Blade, Übersetzungsdateien, dem Bauwerkzeug für Oberflächendateien und den veröffentlichten Anlagen ist die Stelle, an der Upgrades am sichtbarsten scheitern, weil ein Fehler dort sofort jeder sieht.

Drei Punkte gehören auf die Liste, bevor jemand schätzt.

Das Bauwerkzeug. Der Wechsel des Werkzeugs für Oberflächendateien ist kein reines Laravel-Thema und trotzdem Teil des Upgrades. Er betrifft die Konfiguration, die Einbindung in den Vorlagen und den Bauschritt in der Pipeline. Er lässt sich getrennt vom Framework-Sprung machen, und genau das empfehle ich: erst die Oberflächenkette, dann das Framework, weil sonst bei jedem Fehler zwei Ursachen infrage kommen.

Veröffentlichte Anlagen von Paketen. Dateien, die ein Paket einmal in das Projekt kopiert hat und die dort seitdem verändert wurden. Sie werden beim Update nicht aktualisiert, und wer sie überschreibt, verliert die Änderungen. Ein Vergleich gegen die Fassung im Paket findet das in Minuten.

Vorlagen, die auf Interna zeigen. Eine Blade-Vorlage, die eine Klasse des Frameworks direkt anspricht oder auf eine interne Struktur zugreift. Das fällt in keiner Analyse auf und bricht beim Sprung.

Bei einer Plattform mit Erweiterungen von Dritten liegt dieselbe Arbeit noch einmal anders: Shopware-5-Plugins bei der Migration auf 6.

Falle 4: der Projektrahmen läuft auseinander

Das ist die Falle, die am seltensten geplant wird und bei älteren Projekten den größten Einzelposten ausmacht.

Konfigurationsdateien, die Startdatei, die Ausnahmebehandlung, die Dienstregistrierung: All das wurde beim Anlegen des Projekts aus einer Vorlage kopiert. Die Vorlage hat sich seitdem geändert, das Projekt nicht, und nichts an dieser Differenz meldet sich.

Sichtbar wird sie durch einen Vergleich gegen ein frisches Projekt derselben Zielversion:

composer create-project laravel/laravel:^12.0 /tmp/frisch --no-scripts

for d in config bootstrap app/Providers app/Exceptions; do
  echo "=== $d"
  diff -rq "/tmp/frisch/$d" "$d" 2>/dev/null | grep -v "^Only in $d"
done

Die Ausgabe ist beim ersten Mal lang und sortiert sich in zwei Gruppen. Was im Projekt bewusst angepasst wurde, bleibt und wird notiert. Was nur alt ist, wird übernommen. Die Unterscheidung braucht jemanden, der das Projekt kennt, und sie ist an einem Tag zu machen.

Bei Projekten, die mehrere Hauptversionen zurückliegen, ist es oft schneller, den umgekehrten Weg zu gehen: ein frisches Projekt der Zielversion anlegen und den eigenen Code hineinziehen, statt den alten Rahmen Schritt für Schritt anzuheben. Das ist ausdrücklich kein Neubau, weil die Anwendung dieselbe bleibt; es ist ein Wechsel des Rahmens, und er ist an einem Tag prüfbar.

Den mechanischen Teil übernimmt ein Werkzeug, wenn man es eng zuschneidet: Rector im Altprojekt.

Die Reihenfolge, die sich bewährt hat

Aus den vier Fallen folgt eine Abfolge, die die Fehlersuche kurz hält.

  1. Golden Master anlegen für die Ausgaben, die das Geschäft sieht. Vor allem anderen, denn er ist der Maßstab für jeden folgenden Schritt.
  2. Die Oberflächenkette getrennt erledigen, in einer eigenen Auslieferung, auf der alten Framework-Version.
  3. Pakete klären, aufgegebene zuerst. Danach steht der Umfang fest.
  4. Eine Hauptversion, nicht zwei. Auch hier gilt: Ein Projekt, das nach drei Wochen produktiv auf der nächsten Version läuft, hat einen Wert gesichert.
  5. Den Rahmen angleichen, mit dem Vergleich von oben, als eigener Commit.
  6. Golden Master erneut laufen lassen. Jede Abweichung ist ein Fund, und jeder Fund bekommt eine Entscheidung: beabsichtigt oder Fehler.

Der zweite Punkt ist der, der am meisten Zeit spart, und der, der am häufigsten weggelassen wird, weil er sich wie ein Umweg anfühlt.

Zur PHP-Version darunter: Sie hat ihren eigenen Takt und ihre eigenen Brüche, und sie gehört nicht in dieselbe Auslieferung. Was beim PHP-Sprung wirklich auf Sie zukommt, steht in einem eigenen Artikel. Wie ich ein Laravel-Upgrade aufsetze, steht auf einer eigenen Seite.

Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.