Sich in eine Symfony-Anwendung einzuarbeiten, die jemand anderes gebaut hat, ist eine Fähigkeit, die niemand lehrt und die die meisten Engineers auf dem langsamen Weg lernen: zufällige Dateien öffnen, lesen, bis sie verwirrt sind, hoffen, dass ein Muster auftaucht, aufgeben, einen Kollegen fragen. Sechs Wochen später sind sie produktiv und könnten Ihnen nicht sagen, warum.
Ich habe in den letzten zehn Jahren als Berater in etwa dreißig Symfony-Anwendungen Onboarding gemacht. Manche davon waren zweiwöchige Engagements, in denen ich bis Mittwoch nützlich sein musste. Das Muster, wie man eine unbekannte Symfony-Codebase liest, hat konvergiert. Dieser Essay ist der Pfad, den ich gehe, in der Reihenfolge, in der ich ihn gehe, mit den Fragen, die ich an jedem Schritt stelle.
Das ist kein Symfony-Tutorial. Es setzt voraus, dass Sie das Framework kennen. Es geht um die Meta-Fähigkeit, eine Anwendung zu kartieren, die Sie nie gesehen haben, und zu der Art von Verständnis zu kommen, die Sie selbstbewusst die Teile ändern lässt, die geändert werden müssen, ohne die Teile zu zerbrechen, die nicht geändert werden müssen.
Schritt 1: niemals in src/ anfangen
Der Instinkt ist, src/ zu öffnen und zu lesen. Widerstehen Sie. src/ ist, wo die Logik der Anwendung lebt, aber nicht, wo die Form der Anwendung lebt. Die Form lebt in fünf Dateien im Projekt-Root oder in der Nähe.
Lesen Sie diese, in dieser Reihenfolge:
composer.json. Welche Abhängigkeiten sind geladen? Symfony-Version, Doctrine-Version, PHP-Version, aber auch der lange Schwanz. Das Vorhandensein von api-platform/core sagt Ihnen, dass die API-Oberfläche wahrscheinlich ressourcenförmig ist. Das Vorhandensein von oneup/uploader-bundle sagt Ihnen, dass irgendwo Uploads existieren. Das Vorhandensein von nelmio/api-doc-bundle sagt Ihnen, dass jemand sich um API-Dokumentation gekümmert hat. Das Vorhandensein von drei verschiedenen Test-Frameworks sagt Ihnen etwas anderes.
config/bundles.php. Die Bundle-Liste ist eine prägnantere Zusammenfassung von “was kann diese Anwendung” als composer.json. Symfony Mailer registriert? Es gibt irgendwo ein Benachrichtigungssystem. Workflow-Komponente registriert? Es gibt State Machines. SecurityBundle natürlich registriert, aber wie viele Firewalls? Überfliegen und notieren.
config/services.yaml und der Inhalt von config/packages/. Hier lebt der Charakter der Anwendung. Standard-Service-Konfiguration, Autowiring-Ausnahmen, eigene Container-Parameter, umgebungsspezifische Overrides. Nach zehn Minuten hier wissen Sie, was die Konventionen des Teams sind: sind Services autowired? Sind Interfaces auf Implementierungen aliassed? Gibt es viele Compiler-Passes?
config/routes.yaml und jede Routen-Konfiguration in config/routes/. Welche URL-Oberfläche stellt die Anwendung bereit? Sind es MVC-Controller, API-Platform-Ressourcen oder beides? Gibt es Routen-Präfixe, die die App segmentieren (/admin, /api/v1, /webhook)? Die Routen-Tabelle ist das Inhaltsverzeichnis der Anwendung.
.env und .env.local.dist (oder welche Datei committet ist). Was konfiguriert die Anwendung aus der Umgebung? Datenbank, Mail-Transport, Drittanbieter-API-Keys, Feature-Flags. Das sagt Ihnen, welche externen Abhängigkeiten existieren, was oft eine ehrlichere Antwort ist als composer.json (was auflistet, was verwendet werden könnte) oder Service-Konfiguration (was auflistet, was verdrahtet ist).
Am Ende dieser fünf Dateien sollten Sie beantworten können:
- Was für eine Art Anwendung ist das? (CLI-Tool, klassische Web-App, API, hybrid?)
- Womit spricht sie? (Datenbank, Mail, Drittanbieter-APIs, Queues?)
- Was ist der Coding-Stil des Teams? (Stark autowired? Explizit konfiguriert? Viele Compiler-Passes?)
Das ist die Orientierung. Jetzt können Sie src/ öffnen.
Schritt 2: gehen Sie die Entrypoints, nicht die Module
Eine Symfony-Anwendung hat genau vier Arten von Entrypoint: HTTP-Requests, Console-Commands, Message-Handler und Event-Subscriber (die von einem der ersten drei ausgelöst werden). Alles andere ist etwas, das einer dieser vier aufgerufen hat.
Lesen Sie sie in dieser Reihenfolge:
Controller und Routen. Öffnen Sie src/Controller/. Sie lesen noch nicht die Implementierungen; Sie listen sie auf. Welche Endpunkte existieren? Was suggerieren ihre Namen? Clustern Sie sie: src/Controller/Api/User/* ist ein Cluster, src/Controller/Admin/* ein anderes. Nach fünfzehn Minuten zeichnen Sie die Cluster-Karte: “die Anwendung hat grob fünf Oberflächen: öffentliches Web, Kunden-Dashboard, Admin-Panel, öffentliche REST-API und Webhook-Empfänger.”
Console-Commands. Öffnen Sie src/Command/. Das sind die Cron-Jobs, die Daten-Importe, die Wartungsskripte. Die Command-Liste verrät die Architektur oft besser als die Controller, weil Commands die Teile der Anwendung sind, um die sich Operations-Engineers kümmern. Ein app:billing:run-Command sagt Ihnen, dass Billing asynchron ist; ein app:fix:orphaned-orders-Command sagt Ihnen, dass es eine Klasse von Bugs gegeben hat, die einen permanenten Fix-Skript wert war.
Message-Handler. Öffnen Sie src/MessageHandler/ (oder wo auch immer das Team sie platziert). Das sind die asynchronen Operationen. Jeder Handler ist eine Geschichte: “wenn X passiert, tun wir Y.” Die Liste der Handler ist die Liste der Nebeneffekte, die die Anwendung außer der Reihe ausführt. Wenn es 30 Handler gibt und Sie derjenige sind, der eine Integration hinzufügen soll, lesen Sie dieses Verzeichnis sorgfältig.
Event-Subscriber. Öffnen Sie src/EventSubscriber/ und greppen Sie nach #[AsEventListener]. Subscriber sind die Art des Frameworks zu sagen “das läuft als Nebeneffekt von etwas anderem”. Doctrine-Lifecycle-Events, Kernel-Events, eigene Domain-Events. Subscriber sind, wo Kopplung lebt, die nicht in Stack Traces auftaucht, und sie sind, wo Überraschungen leben. Lesen Sie diese jetzt, damit sie Sie nicht später überraschen.
Für jeden Entrypoint lesen Sie nicht die Implementierung. Lesen Sie den Konstruktor, den Docblock und die ersten drei Zeilen des Bodys. Sie suchen: welche Services zieht er rein, was behauptet sein Name zu tun, was tut er tatsächlich auf der obersten Ebene. Das reicht.
Am Ende dieses Schritts sollten Sie eine Entrypoint-Karte zeichnen können: eine Liste jeder Möglichkeit, wie ein Request in die Anwendung gelangen kann, was er behauptet zu tun, und was seine Abhängigkeiten erster Ordnung sind.
Schritt 3: folgen Sie den Daten, nicht dem Aufrufgraphen
Die Versuchung ist jetzt, Implementierungen top-down zu lesen: einen Controller wählen, seinen Aufrufen folgen, die verwendeten Services lesen, die Services lesen, die diese verwenden. Das produziert ein Bild des Aufrufgraphen, was interessant, aber nicht nützlich ist.
Was nützlich ist: der Datengraph. Welche Entities existieren, wie sehen sie aus, welche Beziehungen haben sie, wo leben sie im Lebenszyklus der Anwendung.
Öffnen Sie src/Entity/. Lesen Sie die Entities in dieser Reihenfolge:
- Die Entity, die in den meisten Beziehungen erwähnt wird. Das ist meist
User,Account,TenantoderOrganization. Das ist der Schwerpunkt der Anwendung. Lesen Sie sie sorgfältig. - Die Entity mit den meisten Feldern. Das ist meist das wichtigste Domänenobjekt der Anwendung:
Order,Article,Project, je nach Domäne. Lesen Sie sie sorgfältig. - Die Entities, die zu diesen beiden in Beziehung stehen. Überfliegen. Beziehungen notieren.
- Alles andere. Überfliegen. Sie suchen “interessante” Entities: solche mit State Machines, solche, die wie Audit-Logs aussehen, solche, die wie Outbox-Muster aussehen.
Das Entity-Modell ist das Domänenmodell der Anwendung, auch wenn die Anwendung es nicht so nennt. Zwei Stunden Lesen der Entities orientieren mehr als zwei Tage Lesen der Controller, weil jeder Controller, Command und Handler auf diesen Objekten operiert.
Dann öffnen Sie migrations/. Lesen Sie die letzten fünf oder sechs. Die Migrationshistorie ist die Entwicklungshistorie. Sie können sehen, welche Features kürzlich hinzugefügt wurden, was umbenannt wurde, was deprecated wurde. Eine Migration Version20240312_AddTenantToEverything sagt Ihnen, dass die Anwendung im März 2024 multi-tenant gemacht wurde, und es wird irgendwo in der Codebase eine entsprechende Architekturentscheidung geben.
Schritt 4: stellen Sie die Fragen, die den tragenden Code finden
Inzwischen haben Sie eine Karte der Entrypoints und eine Karte des Datenmodells. Der nächste Schritt ist, die Teile des Codes zu finden, die tragend sind, was nicht immer die Teile sind, die interessant aussehen.
Die Fragen, die ich stelle, in etwa dieser Reihenfolge:
Wo ist die Security? config/packages/security.yaml. Lesen Sie jede Firewall, jede Access-Control-Regel, jeden Voter. Das ist, wo die Anwendung die Vertrauensentscheidungen trifft, und wo Fehler die höchsten Kosten haben. Wenn es einen eigenen Authenticator gibt, lesen Sie ihn; eigene Authenticators sind, wo Security-Bugs sich verstecken.
Wo ist das Geld? Greppbare Begriffe: payment, invoice, charge, subscription, stripe, paypal. Der Geldpfad ist der Code mit den höchsten Einsätzen in den meisten Anwendungen, und das Team kennt ihn meist gut, aber Sie sollten ihn auch kennen. Lesen Sie jeden Controller, Command und Handler, der involviert ist.
Wo ist die Multi-Tenancy? Greppbar: tenant, organization, account_id. Das Tenancy-Modell ist die wahrscheinlichste Quelle von Cross-Customer-Datenlecks. Lesen Sie Doctrine-Filter-Konfigurationen (typischerweise Klassen, die Doctrine\ORM\Query\Filter\SQLFilter erweitern), lesen Sie die Repositories, suchen Sie nach Stellen, an denen Queries gebaut werden, ohne durch den Tenant-Filter zu gehen.
Wo sind die Integrationen? src/Bridge/, src/Integration/, src/External/ oder wo immer das Team sie hingelegt hat. Jeder externe Service ist eine Stelle, an der a) der Vertrag sich ändern kann, ohne dass das Team es merkt, und b) Bugs schwer zu debuggen werden, weil die Daten nicht in der Datenbank der Anwendung liegen. Lesen Sie die Integrations-Adapter; verstehen Sie, welche Timeouts, Retry-Policies und Fehlerbehandlung sie haben.
Wo ist die Queue? config/packages/messenger.yaml plus die Message-Handler. Welche Transports existieren? Was ist async? Wie ist die Retry-Policy? Asynchrone Arbeit ist, wo sich Bugs verstecken, deren Reproduktion drei Tage dauert.
Wo sind die Cron-Jobs? crontab, oder config/packages/scheduler.yaml, oder welcher Scheduling-Mechanismus auch immer verwendet wird. Die Cron-Liste ist der Herzschlag der Anwendung. Wenn ein Cron stehen bleibt, was hört auf zu funktionieren?
Diese sechs Fragen beantworten “wo ist die Anwendung am wahrscheinlichsten zu brechen, und wo wird der Bruch am meisten wehtun.” Der Schnittpunkt davon ist der tragende Code.
Schritt 5: lesen Sie die Seams, nicht die Oberfläche
Der letzte Schritt, bevor Sie anfangen, Code zu ändern, ist, die Seams zu lesen. Ein Seam ist eine Stelle, an der das Verhalten der Anwendung geändert werden kann, ohne den umliegenden Code zu bearbeiten: Dependency-Injection-Punkte, Event-Subscriber, Decorator-Definitionen, Compiler-Passes. Seams sind die Hebel.
Greppbare Muster:
grep -r "AsDecorator" src/ # Decorator-Definitionen
grep -r "decorates:" config/ # das YAML-Konfigurations-Äquivalent
grep -r "AsEventListener" src/ # Event-Listener
grep -r "AutowireDecorated" src/ # Services, die andere dekorieren
grep -r "interface " src/ # die eigenen Interfaces der Anwendung
Jedes Interface in src/ ist eine Frage: was implementiert es, und ist diese Implementierung austauschbar? Die Antwort ist der Unterschied zwischen “ich kann Verhalten ändern, indem ich eine neue Klasse schreibe” und “ich muss jeden Aufrufer finden und alle ändern.”
Ein nützliches Muster: wenn ein Interface nur eine Implementierung hat, fragen Sie, warum das Interface existiert. Manchmal lautet die Antwort “zum Testen” (legitim, deutet auf einen Seam hin, der für Test-Doubles gedacht ist). Manchmal “wir wollten entkoppeln, haben das zweite Stück aber nie gemacht.” Manchmal ist es Überrest einer Architekturentscheidung, die aufgegeben wurde. Jede Antwort sagt Ihnen etwas über die Geschichte der Codebase.
Das Decorator-Muster ist es besonders wert, explizit zu suchen. Ein dekorierter Cache, ein dekorierter Mailer, ein dekorierter Security-Voter: jeder ist eine Stelle, an der das Team das Verhalten des Frameworks gebogen hat, ohne den Code des Frameworks zu bearbeiten, und jeder ist etwas, das Sie wissen müssen, bevor Sie eigenes Verhalten hinzufügen.
Schritt 6: die Diagnose-Fragen
Nachdem Sie alles oben getan haben, sollten Sie diese ohne weiteres Greppen beantworten können. Wenn Sie es nicht können, gehen Sie zum entsprechenden Schritt zurück.
- Was für eine Art Anwendung ist das?
- Was sind ihre Haupt-Entrypoints, in Cluster gruppiert?
- Was ist das Zentrum des Datenmodells?
- Wo wird Multi-Tenancy durchgesetzt (falls überhaupt)?
- Wo ist der Geldpfad?
- Welche Integrationen hat sie, und was sind ihre Failure Modes?
- Was läuft asynchron, und wie ist die Retry-Policy?
- Was läuft auf Cron, und was hört auf zu funktionieren, wenn Cron aufhört?
- Welche Seams existieren, um Verhalten zu ändern, ohne Code zu bearbeiten?
- Was ist die Test-Haltung des Teams, und wo sind die Tests, denen es vertraut?
Zwanzig Antworten, je zehn Minuten zu finden. Das ist das Orientierungs-Budget. Es vorne auszugeben spart Ihnen die langsame Anhäufung von “ich habe das gelernt, als ich es kaputtgemacht habe”-Wissen, das drei Monate Fehlstarts und vermeidbare Incidents kostet.
Was mit der Karte zu tun ist
Der Punkt der Karte ist nicht, sie zu bewundern. Es ist, bessere Entscheidungen über drei Dinge zu treffen:
Wo Verhalten hinzufügen. Ein neues Feature, das nahe am Zentrum des Datenmodells lebt, einen bestehenden Seam verwendet und auf einem etablierten Cron läuft, ist günstig. Dasselbe Feature, das Security-Konfiguration, Queue-Routing und eine schlecht getestete Integration anfasst, ist teuer. Die Karte sagt Ihnen, welches Sie betrachten, bevor Sie sich auf einen Liefertermin festlegen.
Wo refactoren. Refactoring von Legacy-Code ist teuer und riskant. Refactoring von Code, der nicht tragend ist, ist günstig und sicher. Die Karte sagt Ihnen, was was ist. Die siebt-meistgenutzte Utility-Klasse kann an einem Wochenende ohne viel Risiko refactored werden; der Order-Processing-Handler nicht, und jedes Team, das denkt, er könne es, hat die Entrypoint-Karte nicht gelesen.
Was in Ruhe lassen. Die wichtigste Ausgabe der Karte ist die Liste des Codes, den Sie nicht anfassen werden. Manche Teile einer geerbten Symfony-Codebase haben drei Jahre Produktions-Traffic, zwei Team-Wechsel und ein Major-Version-Upgrade überlebt. Sie sind seltsam, aber funktionieren. Die Karte sagt Ihnen, wann “seltsam, aber funktioniert” das richtige Urteil ist und wann es das Symptom eines Problems ist, das es wert ist, behoben zu werden.
Die Superkraft eines Senior-Engineers ist nicht, das Framework besser zu kennen als der Nächste. Es ist, eine unbekannte Codebase schnell genug zu lesen, um in der zweiten Woche eine nützliche Änderung zu machen statt im zweiten Monat. Eine Woche damit zu verbringen, die Codebase zu kartieren, bevor Sie etwas ändern, sieht am Anfang des Engagements langsamer aus und ist in der dritten Woche schneller. Ich habe dieses Experiment dreißig Mal durchgeführt. Die Teams, die das Kartieren nicht überspringen, gewinnen den Zeitplan immer, auch wenn sie im ersten Sprint langsamer aussehen.
Wenn Sie kurz davor stehen, sich in eine unbekannte Symfony-Codebase einzuarbeiten, und eine strukturierte Zweitmeinung wollen, bevor Sie sich auf eine Roadmap festlegen, enthält unser Monolith-Modernisation-Engagement ein einwöchiges Codebase-Audit, das diese Karte für Sie produziert, mit annotierten Entrypoints, einer Datenmodell-Übersicht und einer Liste tragenden Codes, nach Kritikalität bewertet.
Referenzen
- Symfony Best Practices : die offiziellen Konventionen des Frameworks, nützlich, um zu erkennen, wo eine Codebase ihnen folgt oder von ihnen abweicht.
- Doctrine ORM Filter-Dokumentation : der Multi-Tenancy-Durchsetzungsmechanismus, den die meisten Symfony-Codebases verwenden, und einer der ersten Orte, die beim Review der Tenant-Isolation zu prüfen sind.
- Symfony Messenger Dokumentation : Referenz für die asynchrone Komponente, einschließlich Transports und Retry-Strategien, oft das Subsystem mit dem höchsten Hebel in einer Codebase.
- Symfony Service-Decoration : das offizielle Decorator-Muster des Frameworks, der häufigste Seam in reifen Codebases.
- Working Effectively With Legacy Code von Michael Feathers : die kanonische Referenz zu Seams und zum Ändern von Code, den Sie nicht geschrieben haben.