Composer Update ohne Angst: Eine sichere Routine für Legacy-PHP-Projekte

Sandor Farkas
Gründer & Lead Developer
Experte für Softwareentwicklung und Legacy-Code-Optimierung
LinkedInComposer update auf einer fünf Jahre alten Symfony- oder Laravel-Codebasis auszuführen fühlt sich weniger wie Routinewartung an und mehr wie Entschärfung. Die Angst ist nicht irrational. Ein Abhängigkeitsgraph, den niemand seit Jahren angefasst hat, kann Pakete verstecken, die nur noch funktionieren, weil sich nichts um sie herum bewegt hat, und ein einzelner Minor-Versionssprung kann in einem Dutzend fehlschlagender Tests enden, oder schlimmer, in gar keinen fehlschlagenden Tests, weil die Testabdeckung nie da war, um das aufzufangen. Eine sichere composer update Routine für Legacy-PHP ist kein einmaliger Fix, den man einmal ausführt und vergisst. Es sind vier Gewohnheiten, die zusammen praktiziert werden: saubere Lock-File-Hygiene, kleine Batches statt eines riesigen Sprungs, ein Testabdeckungsboden, dem man tatsächlich vertraut, und ein Rollback-Plan, der nicht davon abhängt, dass sich jemand erinnert, was er vor drei Stunden getan hat.
Die composer update Routine für Legacy-PHP beginnt mit der Lock-Datei
Bevor Sie eine einzige Abhängigkeit anfassen, bestätigen Sie, dass composer.lock im Repository eingecheckt ist und tatsächlich zu composer.json passt. Führen Sie composer validate --no-check-all aus und achten Sie auf Abweichungen: ein Projekt, bei dem Produktions-Installationen mit --no-dev laufen, während die Maschine eines Entwicklers vor Jahren bei einem hastigen Deploy etwas leicht Anderes gezogen hat. Diese Art von Diskrepanz ist bei Projekten üblich, die mehrfach den Besitzer gewechselt haben, und sie bedeutet, dass die Lock-Datei im Repository kein verlässlicher Nachweis dafür ist, was in Produktion tatsächlich läuft.
Sobald die Lock-Datei vertrauenswürdig ist, führen Sie composer outdated -D aus, um nur direkte Abhängigkeiten zu sehen, nicht den vollen transitiven Baum, der bei einem gereiften Projekt in die Hunderte von Einträgen gehen kann und Ihnen fast nichts Nützliches sagt. Für jedes Paket, bei dem Sie unsicher sind, zeigt composer why vendor/package, was davon abhängt, und composer why-not vendor/package 4.0 zeigt, was ein Upgrade blockiert. Bei einem Legacy-Projekt bringen diese beiden Befehle meist schneller die eigentliche Einschränkung zum Vorschein als das Lesen von Changelogs: ein vergessenes internes Bundle, das noch an eine alte Guzzle-Version gepinnt ist, oder eine Testbibliothek, die niemand mehr nutzt und die stillschweigend drei andere Pakete zurückhält.
Der Lock-File-Diff selbst ist es wert, separat betrachtet zu werden, getrennt von der composer.json-Änderung, die ihn ausgelöst hat. Ein einzelner direkter Abhängigkeits-Sprung kann ein Dutzend transitive Pakete mitziehen, und bei einem Legacy-Projekt ist dieser Diff oft das erste Mal, dass sich jemand genau ansieht, was die Anwendung unter der Oberfläche tatsächlich an Abhängigkeiten hat. Ihn vor dem Merge zu prüfen, fängt den seltenen Fall ab, in dem ein transitives Paket ganz von sich aus, nebenbei, einen Major-Versionssprung macht, als Nebeneffekt von etwas, das wie ein Minor-Update aussah.
In Batches aktualisieren, nach Risiko gruppiert, nicht alles auf einmal
Ein nacktes composer update ohne Argumente löst den gesamten Abhängigkeitsbaum auf einmal auf, und bei einem Legacy-Projekt bedeutet das, einen Diff mit Dutzenden vermischten Versionssprüngen zu prüfen, Patch-Releases neben Major-Releases, in einem einzigen Pull Request, den niemand sinnvoll prüfen kann. Schränken Sie den Befehl stattdessen ein: composer update vendor/package another-vendor/package aktualisiert nur die genannten Pakete, was aus einer beängstigenden Alles-oder-Nichts-Operation etwas macht, das Sie bewusst sequenzieren können.
Gruppieren Sie Pakete nach Risikostufe, bevor Sie irgendetwas ausführen. Patch-Releases kommen durchgängig zuerst, da sie am unwahrscheinlichsten etwas kaputt machen und den leichten Boden räumen. Minor-Versionssprünge kommen als Nächstes, gruppiert nach Subsystem, Logging-Pakete zusammen, Templating-Pakete zusammen, damit ein fehlschlagender Test auf einen offensichtlichen Verdächtigen zeigt statt eine Bisektion über eine unzusammenhängende Menge von Änderungen zu erzwingen. Major-Versionssprünge erhalten jeweils ihren eigenen Pull Request, für sich geprüft, denn genau dort leben die tatsächlichen Breaking Changes. Diese Batching-Disziplin ist die manuelle Ergänzung zu dem, was wir bereits darüber geschrieben haben, warum automatisierte Abhängigkeits-Updates trotzdem einen Menschen brauchen, der die Warteschlange prüft: Dependabot oder Renovate öffnen bereitwillig fünfzehn Pull Requests an einem Nachmittag, und sie vor dem Merge nach Risiko zu gruppieren ist der Teil, den der Bot nicht für Sie übernehmen kann.
Führen Sie composer audit gegen jeden Batch aus, bevor Sie mergen, statt bis zum Ende des Quartals zu warten. Es prüft installierte Pakete gegen bekannte Sicherheitshinweise und dauert Sekunden, und ein markiertes Paket in einem Batch von fünf zu finden ist ein viel kleineres Problem, als es in einem Batch von dreißig vergraben zu finden. Kombinieren Sie das mit der statischen Analyse, die das Projekt bereits nutzt, PHPStan oder Psalm, denn ein Typfehler, der durch einen geänderten Rückgabetyp eines aktualisierten Pakets eingeführt wird, zeigt sich dort meist früher als in einem Test.
Der Testabdeckungsboden, den Sie vor dem Start brauchen
Volle Code-Abdeckung ist hier nicht die Hürde, und eine anzustreben, bevor Sie ein Abhängigkeits-Update anfassen, ist ein guter Weg, nie anzufangen. Was Sie brauchen, ist enger gefasst: Integrationstests rund um die Stelle, an der jedes riskante Paket tatsächlich genutzt wird. Wenn Ihre Anwendung an drei Stellen eine PDF-Bibliothek aufruft, brauchen Sie Tests, die genau diese drei Aufrufstellen mit realistischem Input durchlaufen, keinen Unit-Test, der die Bibliothek komplett wegmockt.
Wo diese Abdeckung nicht existiert, schreiben Sie sie vor dem Update, nicht danach. Ein kurzer Smoke-Test, der den echten Integrationspunkt aufruft und prüft, ob die Ausgabe stimmt, dauert eine Stunde zu schreiben und sagt Ihnen sofort, ob ein Versionssprung ein Verhalten geändert hat, auf das Sie sich verlassen. Hier verdient sich auch Rector seinen Platz in derselben Routine: Sobald ein Paket-Update eine API-Änderung über die gesamte Codebasis hinweg erzwingt, kann ein automatisierter Refactoring-Durchlauf den mechanischen Teil der Korrektur konsistent über jede Aufrufstelle anwenden, sodass Ihre Smoke-Tests nur noch das Verhalten bestätigen müssen, statt jedes Vorkommen von Hand zu bearbeiten.
Ein Rollback-Plan, der unter Druck hält
„Wir haben Git" ist kein Rollback-Plan. Ein Rollback-Plan ist ein geschriebenes, zweiminütiges Verfahren, dem jemand folgen kann, ohne klar denken zu müssen, denn der Moment, in dem Sie ihn brauchen, ist meist auch der Moment, in dem sich in Produktion schon etwas seltsam verhält. Taggen Sie den Commit, bevor jeder Batch beginnt. Halten Sie die vorherige composer.lock irgendwo verfügbar, wo ein Deploy-Skript direkt darauf zugreifen kann, zusätzlich zur Git-Historie, damit die Wiederherstellung der genauen Abhängigkeitsversionen, die vorher liefen, composer install gegen die alte Lock-Datei ist, statt einer manuellen Rekonstruktion dessen, auf welcher Version alles war.
Entscheiden Sie im Voraus, was einen Rollback auslöst, statt in dem Moment zu entscheiden, in dem bereits etwas brennt. Ein Ausschlag der Fehlerrate im Deploy-Dashboard, ein fehlgeschlagener Smoke-Test in Staging, der vor einer Stunde noch bestanden hat, oder ein Support-Ticket, das zu einem bekannten Regressionsmuster aus dem Changelog passt, das Sie vorher gelesen haben, jedes davon sollte reichen, um zuerst zurückzurollen und danach zu untersuchen, nicht umgekehrt.
Wann sich das nicht mehr lohnt, selbst zu machen
Ein monatlicher oder vierteljährlicher Rhythmus für diese Routine ist für die meisten Teams realistisch, und der tatsächliche Zeitaufwand pro Zyklus sinkt schnell, sobald die Lock-Datei sauber ist und die Batching-Gewohnheit etabliert ist. Was nicht sinkt, ist der Beurteilungsteil: entscheiden, welche Pakete wichtig genug sind, um sorgfältig getestet zu werden, ein Changelog gut genug lesen, um zu wissen, ob ein Major-Sprung sicher ist, erkennen, wann ein scheinbar routinemäßiges Update tatsächlich für etwas Kritisches tragend ist. Dieser Teil wird nicht leichter, nur weil Sie es schon einmal gemacht haben, und in einem Team, in dem niemand das konsequent verantwortet, rutscht er tendenziell genau in dem Sprint weg, in dem es am wenigsten verkraftbar ist.
Wenn Ihr Team nicht die Kapazität hat, diesen Zyklus zuverlässig jedes Quartal zu fahren, ist das einer der konkreteren Bausteine eines laufenden Code-Qualitäts-Consulting-Engagements, bei dem jemand außerhalb des regulären Sprint-Zyklus den Abhängigkeitsbaum gesund hält, statt dass er alle achtzehn Monate zu einem einmaligen Notfall wird. In jedem Fall ändert sich die Routine selbst nicht: eine saubere Lock-Datei, gebatchte Updates, ein Testabdeckungsboden, dem Sie vertrauen, und ein Rollback-Plan, der geschrieben ist, bevor Sie ihn brauchen, nicht improvisiert, während bereits etwas kaputt ist.
Wenn Sie eine zweite Meinung zu Ihrer aktuellen composer.lock möchten, oder Hilfe beim Aufbau dieser Routine in einen Rhythmus, an dem Ihr Team tatsächlich festhält, schreiben Sie uns an hello@wolf-tech.io oder schauen Sie auf wolf-tech.io vorbei.
