Versteckte Abhängigkeiten in gewachsenen Systemen aufdecken

Versteckte Abhängigkeiten sind der Grund, warum eine kleine Änderung ein ganzes System kippen lässt. So decken Sie sie systematisch auf, bevor sie zuschlagen.

Sie ändern eine scheinbar harmlose Funktion, und drei Wochen später fällt der nächtliche Rechnungslauf aus. Kein Fehler im Code, den Sie angefasst haben. Sondern ein Modul, von dem niemand wusste, dass es sich auf genau dieses Verhalten verlassen hat. Willkommen bei den versteckten Abhängigkeiten in gewachsenen PHP-Systemen.

Ich arbeite seit dreizehn Jahren mit Legacy-PHP-Anwendungen, und mein wichtigster Grundsatz vorweg: Ein System, das seit acht Jahren die Buchhaltung eines Unternehmens trägt, ist kein Müll. Es ist der Beweis, dass die Fachlogik funktioniert. Das Problem ist fast nie die Logik. Das Problem ist der Code drumherum, der diese Logik über die Jahre unsichtbar miteinander verdrahtet hat. Genau diese Verdrahtung machen wir jetzt sichtbar.

Was sind versteckte Abhängigkeiten überhaupt?

Versteckte Abhängigkeiten sind Verbindungen zwischen Teilen Ihres Systems, die nicht im Code sichtbar sind, den Sie gerade lesen. Sie stehen nicht im Konstruktor, nicht im Interface, nicht in der Methodensignatur. Sie wirken trotzdem.

In der Praxis begegnen mir immer wieder dieselben Muster:

  • Globaler Zustand: global $db;, statische Registries, $_SESSION-Zugriffe tief in der Fachlogik. Eine Klasse funktioniert nur, weil vorher irgendwo etwas gesetzt wurde.
  • Implizite Reihenfolge: Datei A muss vor Datei B eingebunden werden, weil B auf eine Konstante oder Funktion aus A baut. Nirgends dokumentiert.
  • Datenbank als geheimer Vertrag: Ein Trigger, eine Stored Procedure oder ein zweites System schreibt in dieselbe Tabelle. Ihr PHP-Code ahnt nichts davon.
  • Seiteneffekte beim Laden: Eine Datei, die beim require sofort eine Session startet, einen Header sendet oder eine Verbindung öffnet.
  • Magische Werte: Ein Statuscode 7, den drei verschiedene Module unterschiedlich interpretieren, aber alle voneinander abhängig.

Das Tückische: Solange nichts geändert wird, laufen diese Abhängigkeiten still und zuverlässig. Sie zeigen sich erst im Moment der Änderung. Deshalb ist Neuentwicklung so verlockend und gleichzeitig so gefährlich: Sie werfen mit dem sichtbaren Code auch das gesamte ungeschriebene Wissen über diese Verbindungen weg.

Warum statische Analyse allein nicht reicht

Der erste Reflex ist ein Tool. Zu Recht, denn statische Analyse deckt viel auf. Nur eben nicht alles. Ich fahre in der Regel zweigleisig: erst die Maschine, dann die Laufzeit.

Mit composer why und composer why-not sehen Sie Paket-Abhängigkeiten. Mit PHPStan auf einem hohen Level fallen undefinierte Variablen, verdeckte Typkonflikte und Zugriffe auf nicht existierende Properties auf, oft genau dort, wo globaler Zustand hereinsickert. Werkzeuge wie deptrac oder das Composer Unused-Plugin zeigen, welche Schichten Ihres Systems eigentlich aufeinander zugreifen, und decken Verbindungen auf, die architektonisch gar nicht sein dürften.

Was diese Werkzeuge aber nicht sehen: alles, was zur Laufzeit dynamisch passiert. $service = new $className();, Aufrufe über Magic Methods wie __call, in Strings zusammengesetzte Klassennamen, Event-Dispatcher, in denen Listener lose registriert sind. Genau hier verstecken sich in gewachsenen Symfony- und Laravel-Anwendungen die unangenehmsten Kopplungen.

Wie decken Sie versteckte Abhängigkeiten zur Laufzeit auf?

Die ehrlichste Methode ist, das System bei der Arbeit zu beobachten. Nicht raten, sondern messen.

  1. Call-Graphs mit einem Profiler: Lassen Sie einen kritischen Ablauf, etwa den Rechnungslauf, einmal komplett unter Xdebug oder Blackfire laufen. Der resultierende Call-Graph zeigt Ihnen, welche Funktion tatsächlich welche aufruft, inklusive der dynamischen Wege, die kein statisches Tool findet.
  2. Datenbank mitschneiden: Aktivieren Sie temporär das General Query Log Ihrer Datenbank oder hängen Sie sich in die PDO-Schicht. So sehen Sie, welche Tabellen ein Vorgang wirklich anfasst, auch die, die im Code nirgends erwähnt scheinen.
  3. Container inspizieren: In Symfony liefert bin/console debug:container und debug:autowiring, in Laravel ein kurzer Blick in die Service Provider, welche Dienste wie verdrahtet sind. Tagged Services und Decorator sind ein Klassiker für Kopplungen, die im Fachcode unsichtbar bleiben.
  4. Nach globalem Zustand greppen: Ein schlichtes grep -rn "global \$" src/ und die Suche nach $_SESSION, $_GET, static :: und define( außerhalb der Konfiguration bringt in einer halben Stunde mehr ans Licht als jedes Meeting.

Wichtig ist die Kombination. Der Profiler zeigt Ihnen das Was zur Laufzeit, die statische Analyse das Wo im Code. Erst zusammen ergeben sie eine belastbare Landkarte.

Der Sicherheitsgurt: Tests, bevor Sie etwas anfassen

Bevor Sie eine einzige Abhängigkeit auflösen, brauchen Sie ein Sicherheitsnetz. Bei Legacy-Code sind das selten saubere Unit-Tests, sondern Characterization Tests: Tests, die nicht prüfen, was der Code tun sollte, sondern festhalten, was er heute tatsächlich tut.

Nehmen Sie den kritischen Ablauf, füttern Sie ihn mit realistischen Eingaben und schreiben Sie die Ausgabe als Soll-Wert fest, auch wenn diese Ausgabe fachlich seltsam wirkt. Dieses seltsame Verhalten ist oft genau die versteckte Abhängigkeit, auf die sich ein anderes Modul verlässt. Approval-Testing mit einem Werkzeug wie spatie/phpunit-snapshot-assertions ist dafür ideal, weil Sie ganze Ausgabestrukturen einfrieren, ohne jeden Wert einzeln zu behaupten.

Erst wenn dieser Gurt sitzt, lösen Sie die Kopplung. In kleinen Schritten: globalen Zugriff durch einen Konstruktor-Parameter ersetzen, den Seiteneffekt aus dem Ladevorgang in eine explizite Methode ziehen, den magischen Wert in eine benannte Konstante oder ein Enum überführen. Nach jedem Schritt laufen die Tests. Fällt einer, haben Sie eine echte Abhängigkeit gefunden, statt eine im Livebetrieb.

Ein realistischer Blick auf Aufwand und Nutzen

Sie werden nicht jede versteckte Abhängigkeit in einer Woche finden, und das ist auch nicht das Ziel. Das Ziel ist, die Abhängigkeiten rund um den Bereich sichtbar zu machen, den Sie als Nächstes ändern wollen. Arbeiten Sie entlang des Schmerzes, nicht entlang eines idealen Architekturbilds.

Der Nutzen ist unmittelbar: Jede aufgedeckte und benannte Abhängigkeit ist ein Risiko weniger im nächsten Deployment. Und in fast allen Fällen, die mir begegnen, zeigt genau diese Arbeit, dass das System eben nicht neu geschrieben werden muss. Die Fachlogik steht. Es ist die Verdrahtung drumherum, die entwirrt gehört.

Praxis-Tipp: Fangen Sie mit dem einen Ablauf an, vor dem sich Ihr Team beim Deployment am meisten fürchtet. Profilen, mitschneiden, mit einem Characterization Test einfrieren. Diese eine Stelle sauber zu verstehen ist mehr wert als eine vollständige Architekturdokumentation, die niemand liest.

Wenn Sie an einem solchen System sitzen und nicht sicher sind, wo Sie den ersten Schnitt ansetzen, ist das genau die Art von Analyse, bei der ich mit LegacyWerk unterstütze.

Abhaengigkeiten sichtbar machen Statische Analyse PHPStan, deptrac Laufzeit-Profiling Xdebug, Blackfire DB-Mitschnitt Query Log, PDO Landkarte der Kopplungen Characterization Test einfrieren dann sicher aendern

Ist Ihre PHP-Anwendung noch zu retten?

Im kostenlosen Kurz-Check schaue ich mit Ihnen auf Ihr System und sage Ihnen ehrlich, was möglich ist – unverbindlich und ohne Verkaufsdruck.

Kurz-Check vereinbaren