Symfony Flex und Recipes: Paket-Upgrades ohne kaputte Konfiguration
Führe in einem frischen Symfony-Projekt composer require symfony/mailer aus, und es passieren drei Dinge, die Composer allein nie tun würde: Unter config/packages/ taucht eine neue Datei auf, in config/bundles.php kommt eine Zeile dazu, und zwei auskommentierte Variablen landen in deiner .env. Das alles erledigt Symfony Flex, und Symfony Flex Recipes sind die Anweisungen, denen es dabei folgt. Die meisten Teams verlassen sich täglich darauf, ohne je eines gelesen zu haben. Das geht gut, bis composer recipes:update eine Konfigurationsdatei anfasst, an der jemand einen Nachmittag lang gefeilt hat, und der resultierende Diff Fragen aufwirft, die niemand im Team beantworten kann.
Den Mechanismus zu verstehen zahlt sich in dem Moment aus, in dem du irgendetwas upgradest. Flex kann Konfigurationsänderungen über Paketversionen hinweg für dich mitnehmen, aber nur, wenn dein Projekt in einem Zustand ist, in dem es diese Arbeit erledigen kann.
Was Symfony Flex Recipes tatsächlich tun
Ein Recipe ist ein kleiner Satz von Anweisungen, der außerhalb des Pakets liegt, das er konfiguriert, und zwar in einem von zwei öffentlichen GitHub-Repositories: symfony/recipes und symfony/recipes-contrib. Wenn du ein Paket installierst, prüft Flex, ob es dafür ein Recipe gibt, und führt in diesem Fall die Aktionen aus dem Manifest des Recipes aus. Die üblichen:
- Standard-Konfigurationsdateien ins Projekt kopieren, typischerweise unter
config/packages/ - Das Bundle in
config/bundles.phpregistrieren, wo nötig pro Environment - Umgebungsvariablen mit auskommentierten Standardwerten an
.envanhängen - Einträge in
.gitignoreoder Skripte im Abschnittauto-scriptsdercomposer.jsonergänzen
Alles, was ein Recipe tut, ist reproduzierbar. Dieselbe Recipe-Version, auf dasselbe Projekt angewendet, erzeugt dieselben Dateien, und diese Eigenschaft ist die Grundlage für alles Weitere in diesem Beitrag. Weil Flex genau weiß, welche Recipe-Version deine config/packages/mailer.yaml erzeugt hat, kann es später berechnen, was sich zwischen dieser Version und einer neueren geändert hat, und nur die Differenz anwenden.
Recipes außerhalb der Pakete zu halten war eine bewusste Designentscheidung. Die Standardkonfiguration kann sich verbessern, ohne auf ein Paket-Release zu warten, und das Symfony-Core-Team prüft jede Änderung am Haupt-Recipe-Repository mit derselben Sorgfalt wie Framework-Code.
symfony.lock ist Zustand, nicht Rauschen
Jedes ausgeführte Recipe wird in symfony.lock im Projektstamm festgehalten, inklusive der exakten Version des Recipes, das gelaufen ist. Diese Datei ist das Gedächtnis des Recipe-Systems. Flex liest sie, um zu entscheiden, ob ein Recipe noch ausgeführt werden muss, und um die ursprünglichen Dateien zu rekonstruieren, wenn du ein Update anstößt.
Committe sie. Behandle sie genau wie composer.lock. Wenn symfony.lock fehlt oder nicht synchron ist, verliert Flex die Fähigkeit, Recipe-Diffs zu berechnen, Teamkollegen werden aufgefordert, Recipes erneut auszuführen, die längst gelaufen sind, und die CI verhält sich anders als lokale Rechner. Eine überraschend große Zahl von Upgrade-Problemen geht auf eine symfony.lock zurück, die jemand vor langer Zeit gelöscht hat, "um aufzuräumen".
Um zu sehen, wo du stehst, führe composer recipes aus. Es listet jedes installierte Recipe auf und markiert diejenigen, für die Updates verfügbar sind. composer recipes symfony/framework-bundle zeigt die Details für ein einzelnes Paket, inklusive eines Links zur exakten Recipe-Version auf GitHub.
Was composer recipes:update wirklich anwendet
Lange Zeit war der einzige Weg, ein Recipe zu erneuern, composer recipes:install <package> --force. Das überschrieb deine Dateien mit den neuen Standardwerten und überließ es dir, deine Anpassungen aus der Git-Historie wiederherzustellen. Der Befehl composer recipes:update hat diesen Workflow durch etwas ersetzt, das einem Git-Merge viel näher kommt.
Unter der Haube tut er das, was du mit genug Geduld von Hand tun würdest. Er erzeugt die Dateien so, wie die alte Recipe-Version sie geschrieben hätte, berechnet den Diff zwischen diesem Zustand und der neuen Recipe-Version und wendet den resultierenden Patch auf deine tatsächlichen Dateien an. Deine Änderungen überleben, solange sie sich nicht mit Zeilen überschneiden, die das Recipe geändert hat. Wenn sie sich überschneiden, schreibt Flex Standard-Konfliktmarker in die Datei, dieselben <<<<<<<-Blöcke, die du aus Git kennst, und überlässt dir die Auflösung.
Ein paar Gewohnheiten machen diesen Workflow unspektakulär. Aktualisiere ein Paket nach dem anderen, auf einem sauberen Working Tree, und committe nach jedem Lauf, damit jede Recipe-Änderung ihren eigenen überprüfbaren Diff bekommt. Lies diesen Diff vor dem Commit, besonders den .env-Teil: Recipes führen manchmal neue Variablen mit Standardwerten ein, die für deine Infrastruktur falsch sind. Lass danach deine Testsuite und bin/console lint:container laufen, denn ein nachlässig aufgelöster Recipe-Konflikt ist ein Config-Bug, dem du sonst in Produktion begegnest.
Angepasste Konfiguration vor Recipe-Updates schützen
Der Merge-Mechanismus schützt dich technisch. Ein paar Praktiken reduzieren Konflikte auf nahezu null.
Bevorzuge Umgebungsvariablen gegenüber Änderungen an Recipe-generierten Dateien. Die meiste generierte Konfiguration referenziert bereits Env-Variablen, %env(DATABASE_URL)% ist das offensichtliche Beispiel. Wenn du deine Anpassung als Wert in .env.local oder in deiner Deployment-Umgebung ausdrücken kannst, bleibt die Recipe-Datei unberührt, und künftige Updates greifen sauber.
Wenn du generierte Dateien doch bearbeitest, halte die Änderungen klein und lokal. Füge deine Schlüssel hinzu, aber formatiere die Datei nicht um, ordne keine Blöcke neu an und entferne nicht die auskommentierten Beispiele, die Recipes mitliefern. Jede Zeile, die du unangetastet lässt, ist eine Zeile, die das nächste Update ohne dein Zutun patchen kann.
Halte wirklich eigene Konfiguration in eigenen Dateien. Alles unter config/packages/ wird automatisch geladen, also können projektspezifische Einstellungen in einer Datei leben, die das Recipe-System nie anfasst.
Und schließlich: Committe symfony.lock zusammen mit den Recipe-Änderungen, die sie beschreibt, in einem Commit, dessen Nachricht das Paket nennt. Wer in zwei Jahren git blame auf config/packages/security.yaml ausführt, wird dir dankbar sein.
Haupt-Recipes, Contrib-Recipes und Pakete ohne beides
Symfony Flex Recipes kommen aus zwei Repositories mit unterschiedlichen Regeln:
| Repository | Gepflegt von | Ausführung |
|---|---|---|
symfony/recipes | Symfony-Core-Team, strenges Review | Läuft automatisch |
symfony/recipes-contrib | Community, leichteres Review | Läuft nach deiner Zustimmung |
Standardmäßig fragt Flex, bevor es ein Contrib-Recipe ausführt, und bietet an, sich deine Antwort zu merken. Wenn du extra.symfony.allow-contrib in der composer.json auf true setzt, akzeptierst du sie dauerhaft. Das ist die übliche Wahl, sobald ein Team dem Mechanismus vertraut.
Ein Paket ohne Recipe ist kein Defekt. Ein Recipe existiert nur, wenn jemand eines geschrieben hat und die Repository-Maintainer es angenommen haben. Viele solide Composer-Pakete haben keines, entweder weil sie keine Verdrahtung brauchen oder weil noch niemand eines eingereicht hat. Die installierst du so wie in jedem PHP-Projekt und schreibst die Konfiguration selbst.
Die Recipe-Repositories definieren auch die Aliase, durch die composer require orm das Doctrine-Pack und composer require logger das Monolog-Bundle installiert. Aliase sind nur Abkürzungen; die installierten Pakete sind identisch. Packs verdienen hier ebenfalls eine Erwähnung: symfony/orm-pack und Verwandte bündeln mehrere zusammengehörige Pakete hinter einem einzigen Require, und composer unpack symfony/orm-pack schreibt die echten Abhängigkeiten in deine composer.json, wenn du ihre Versionen einzeln festpinnen musst.
Größere Organisationen können noch einen Schritt weitergehen und eigene Recipes hosten. Flex liest eigene Endpoints aus extra.symfony.endpoint, womit ein Plattform-Team Standardkonfiguration für interne Bundles über Dutzende Services hinweg ausliefern kann.
Mit Flex von Symfony 6.4 auf 7.x upgraden
Ein Major-Upgrade ist der Moment, in dem sich das Recipe-System bezahlt macht, denn Konfigurationskonventionen ändern sich zwischen Major-Versionen, und Recipes tragen diese Änderungen für dich mit.
Erstens: Erledige die 6.4-Hausaufgaben. Beseitige jede Deprecation, die dein Code auslöst, solange du noch auf 6.4 bist, denn Symfony 7 entfernt, was 6.4 als veraltet markiert. Diesen Teil haben wir in unserem Symfony-7-Upgrade-Leitfaden ausführlich beschrieben.
Zweitens: Bring alle Recipes auf den neuesten Stand, solange du noch auf 6.4 bist. Führe composer recipes aus, dann composer recipes:update für jeden veralteten Eintrag, ein Commit pro Paket. Das vor dem Versionssprung zu tun trennt über Jahre angesammelte Recipe-Drift von den Änderungen, die Symfony 7 tatsächlich verlangt, und hält beide Reviews klein.
Drittens: Hebe die Versionsgrenze an. Flex liest extra.symfony.require in der composer.json, also ändere den Wert von 6.4.* auf 7.4.*, das aktuelle 7.x-LTS, und führe composer update "symfony/*" --with-all-dependencies aus. Wenn Composer unlösbare Konflikte meldet, unterstützt ein Drittanbieter-Bundle Symfony 7 noch nicht. Kümmere dich zuerst um diese Liste; der Versionssprung kommt danach.
Viertens: Aktualisiere die Recipes erneut. Das Recipe von symfony/framework-bundle erzeugt typischerweise den größten Diff: Optionen, die es nicht mehr gibt, verschwinden, umbenannte Schlüssel bekommen ihre neuen Namen, und neue Standardwerte kommen hinzu. Löse Konflikte, indem du das neue Recipe als Absicht liest, also so, wie ein frisches 7.x-Projekt aussieht, und deine Anpassungen darauf überträgst. Dann geht es zum nächsten Paket.
Fünftens: Verifiziere. Führe composer validate, bin/console lint:container und die vollständige Testsuite aus. Konfigurationsfehler zeigen sich beim Kompilieren des Containers, also fängt ein bootender Kernel plus eine grüne Pipeline die meisten Fehler ab, bevor das Deployment es tut.
Für Codebasen, die weiter zurückliegen als 6.4, ist die Mechanik dieselbe, nur der Weg ist länger. Einen gestaffelten Ansatz haben wir in Symfony-Migration ohne Code-Freeze beschrieben.
Recipe-Hygiene ist eine Upgrade-Versicherung
Teams, deren Symfony-Upgrades unspektakulär bleiben, teilen meist unglamouröse Gewohnheiten: symfony.lock ist committet, und Recipe-Diffs werden wie Anwendungscode reviewt, statt durchgewinkt zu werden. Teams, die sich schwertun, entdecken Config-Drift meist mitten im Upgrade, in Form von Dateien, die ein altes Recipe halb generiert hat, manuellen Änderungen, an die sich niemand erinnert, und Umgebungsvariablen, die in Produktion existieren, aber in keiner .env-Datei.
Wenn dein Projekt nach der zweiten Gruppe klingt, ist das Entwirren vor dem nächsten Framework-Release deutlich günstiger als währenddessen. Genau diese Bestandsaufnahme und Aufräumarbeit machen wir bei Wolf-Tech, von Code-Quality-Audits, die den aktuellen Zustand deiner Konfiguration abbilden, bis zur gestaffelten Legacy-Code-Optimierung, wenn Jahre von Drift entwirrt werden müssen. Schreib an hello@wolf-tech.io oder schau auf wolf-tech.io vorbei, und wir sagen dir schnell, ob dein Upgrade ein Wochenende oder ein Quartal ist.

