Symfony 4 und Flex: Erkenntnisse beim Migrieren auf die neue Struktur
![Symphony 4 & Flex Symphony 4 & Flex](/uploads/media/1180x720/04/2074-massive-art-blog-symfony-4-flex.jpg?v=1-0)
Jedes Symfony Projekt wird früher oder später auf die neue Symfony Flex Struktur angepasst. Hier sind meine Erkenntnisse dazu.
Als Symfony Entwickler kommt einem die Dokumentation zum Upgrade auf Flex schnell mal zwischen die Finger. Jedoch ist diese sehr allgemein gehalten und geht (meiner Meinung nach) nicht auf diverse Details ein.
Grundsätzliches Vorgehen zum Upgrade von Symfony 3.x auf >= 4.2
Einmal vorweg: Ich habe hier nur ein Upgrade auf Symfony 4.x probiert und noch nicht mit Symfony 5.x
Allerdings sollte das Prinzip dasselbe sein und auch bei einem Upgrade auf Symfony 5.x funktionieren. Nur dass es hierbei noch mehr "Breaking Changes" gibt.
Bevor man mit dem Upgrade beginnt, sollte man folgende Dateien (sofern vorhanden) sichern:
- .env
- .env.dist
- bin/console
- Weitere modifizierte oder selbst erstellte Dateien im Ordner bin
Nach dem Upgrade muss man – falls notwendig – eventuelle Änderungen etc. manuell in die neuen Dateien zurückführen.
Änderungen an der bin/console sowie eventuelle andere Dateien im bin-Verzeichnis sichern.
Mögliche Änderungen an der bin/console von Beginn an notieren, damit diese nach einem Update wiederhergestellt werden können.
Zusätzlich sollte das Verzeichnis src vor dem Update in src-bundles umbenannt werden.
Diese müssen später unter anderem in eine entsprechend neue Struktur im src-Verzeichnis gebracht werden – hier stört der alte Inhalt beim Update.
Anpassungen im composer.json
Um Symfony zu aktualisieren, sind die folgenden Änderungen im composer.json durchzuführen:
a. Falls Incenteev/ParameterHandler installiert ist, ist dieser zu entfernen, da für die Konfiguration jetzt .env-Dateien beziehungsweise Umgebungsvariablen verwendet werden und nicht mehr das parameters.yml.
- Entfernen von "incenteev/composer-parameter-handler" in require und require-dev Sektionen.
- Entfernen aller Vorkommnisse von "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters" in der scripts-Sektion.
- Entfernen der alten scripts-Einträge, die mit: "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler:: beginnen; hier kommt stattdessen auto-scripts zum Einsatz
b. Neue Einträge im scripts erstellen
"scripts": {
"auto-scripts": {
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
}
Bei diesem Schritt sind eventuell eigene Skripte zu ergänzen. Nach dem Upgrade von Symfony sieht dies dann z.B. so aus
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
}
Nach dem Upgrade kann man in auto-scripts weitere Symfony Befehle ähnlich wie diesen ergänzen bzw. bei assets:install definieren, ob man Kopien der Dateien oder Symlinks verwenden will.
c. In der extra-Sektion müssen die Parameter, die mit symfony- beginnen entfernt werden. Falls man nach dem Update die Struktur ändern will, kann man sich an diese Dokumentation halten.
d. Zusätzlich sollte man folgendes in der extra-Sektion ergänzen:
"symfony": {
"allow-contrib": true,
"require": "4.4.*"
}
a. require definiert hier welche Version für die symfony Komponenten zum Einsatz kommt.
b. allow-contrib definiert, dass Symfony Flex Recipes nicht nur für die offiziellen Symfony Komponenten, sondern auch für jene von anderen Bundles verwendet werden. Welche das sind, findet man hier: Github.
Ist dies nicht gewünscht, muss false gesetzt werden. Ich persönlich würde empfehlen, dass dieser Wert zumindest bis nach dem Upgrade von Symfony auf true gesetzt wird und bei Bedarf danach auf false.
Damit erhält man zumindest die Grundkonfiguration für die meisten der installierten Bundles.
e. Aufgrund der extra Sektion lassen sich die Symfony Dependencies jetzt folgendermassen in
"require": {
"symfony/console": "*",
"symfony/dotenv": "*",
"symfony/flex": "^1.1",
"symfony/framework-bundle": "*",
"symfony/yaml": "*"
}
hinzufügen.
Diese Liste kann bei Bedarf um weitere Dependencies erweitert werden – je nachdem, ob dies für das Projekt notwendig beziehungsweise auch gewollt ist.
f. Um sicherzustellen, dass die komplette Symfony Library nach der Änderung nicht mehr zum Einsatz kommt, muss
"conflict": {
"symfony/symfony": "*"
}
ergänzt werden. Falls es conflict schon gibt, einfach nur "symfony/symfony": "*" ergänzen.
g. Aus autoload folgenden Eintrag entfernen
"classmap": ["app/AppKernel.php", "app/AppCache.php"]
h. Im autoload unter psr-4 folgenden Eintrag ergänzen
"App\\": "src/"
i. Existierende autoload psr-4 Einträge müssen auf src-bundles angepasst werden. Beispielsweise:
"AppBundle\\": "src-bundles/AppBundle/"
Am Schluss sollte ein fertiges composer.json, ähnlich dem des Symfony Skeletons, entstanden sein. Symfony Flex Recipes können hier eingesehen oder gesucht werden bzw. in den Repositories: Symfony Recipes oder Symfony Recipes-Contrib.
Symfony Updaten
Zuerst folgende Verzeichnisse und Dateien löschen:
- bin
- vendor
- symfony.lock (falls vorhanden)
Jetzt kann Symfony mittels Composer aktualisiert werden: COMPOSER_MEMORY_LIMIT=-1 composer update -o
Der Pfad zu Composer hängt von der jeweiligen Installation ab.
Manche der automatisch ergänzten auto-scripts, wie zum Beispiel “Cache löschen”, werden vermutlich noch fehlschlagen. Da die alten Konfigurationen noch nicht geladen werden (siehe den Punkt Konfiguration im Artikel).
Es kann nicht --no-scripts verwendet werden, da ansonsten die Symfony Flex Recipes nicht ausgeführt werden.
COMPOSER_MEMORY_LIMIT=-1 wird verwendet, um zu verhindern, dass Composer durch das Memory-Limit gestoppt wird (aufgrund der vielen Änderungen könnte dies der Fall sein; falls PHP in der CLI hier nicht bereits unbeschränkt ist).
Bundles
Die zu ladenden Bundles werden nicht mehr direkt im Applikationskernel definiert, sondern man kann diese Bundles jetzt in config/bundles.php definieren.
Sollte man nicht vorhaben, die Klassennamen auf das neue Schema zu aktualisieren (siehe dazu den Punkt Namespace für Klassen im Artikel), könnten hier auch eigene Bundles ergänzt werden.
Die Konfiguration sieht folgendermassen aus (hier reduziert auf Beispiele):
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
];
Die eigenen Bundles können ebenso unter Verwendung des Klassennamens, wie oben zu sehen, ergänzt werden. Das pro Bundle-Klasse definierte Array gibt an, für welche Umgebungen das jeweilige Bundle aktiv ist. Wenn man hier all angibt, wird es für alle Umgebungen verwendet.
Konfiguration
Vor der Änderung gab es eine Datei im Symfony Projekt, über welche die komplette Konfiguration geladen wurde. Man konnte diese Konfiguration nur dann aufteilen, wenn man weitere Dateien importierte.
Das lässt sich prinzipiell auch mit der neuen Struktur umsetzen – ist aber eigentlich nicht der gewünschte Weg. Stattdessen sollte man die Konfiguration für die einzelnen Bundles aufteilen.
Hier mein Vorschlag zur Vorgehensweise:
- Bundles über Symfony Flex neu installieren (siehe den Punkt "Bundles" im Artikel), wodurch die Standardkonfiguration erstellt wird. Dies hat den Vorteil, dass der für das Bundle vorgesehene Name der Konfigurationsdatei zum Einsatz kommt.
- Danach habe ich diese mit den Werten der ursprünglichen Konfiguration angepasst.
Diese Konfiguration findet man in config/packages/*.yaml beziehungsweise config/packages/{env}/*.yaml
Parameter wie zum Beispiel,
parameters:
locale: 'en'
für die Anwendung sind jetzt standardmässig in der Datei für die Services definiert. In unserem Projekt haben wir diese allerdings in config/packages/app.yaml gegeben, damit man diese ohne ein config/services_{env}.yaml anzulegen mit config/packages/{env}/app.yaml (z.B. für Tests) überschreiben kann.
In der App\Kernel können auch noch weitere Pfade definiert werden und zwar in der Methode configureContainer.
Services
Die Services der Applikation werden wie gehabt über eine Datei definiert. Man muss diese nur entsprechend verschieben, z.B. nach config/services.yaml
Für Bundles werden diese äquivalent zur Konfiguration in config/packages/*.yaml verwaltet. Es ist auch möglich, pro Umgebung Services über config/services_{env}.yaml zu definieren.
Wie sonst in Symfony Konfigurationen üblich, muss man nicht yaml verwenden, sondern kann zum Beispiel auch XML einsetzen.
Man kann hier auch über glob patterns, also zum Beispiel
imports:
- { resource: 'services/*.{php,xml,yaml,yml}' }
# oder auch möglich:
imports:
- { resource: 'services/**/*.{php,xml,yaml,yml}' }
weitere Dateien oder komplette Ordner laden. So kann man die Services weiter aufteilen und sich Dank autoconfigure und autowiring die Konfiguration der Services oft ganz oder fast komplett sparen.
Routen
Für Routen gibt es jetzt einen eigenen Ordner config/routes/*.yaml, in dem man diese auf mehrere Dateien sinnvoll aufteilen kann. Pro Environment kann man über config/routes/{env}/*.yaml zusätzliche Routen definieren, die nur für diese gültig sind. Anstelle von yaml kann man natürlich, wie sonst auch in Symfony Konfigurationen, z.B. XML verwenden. Zusätzlich kann man im App\Kernel bei Bedarf in der Methode configureRoutes weitere Pfade definieren.
Variablen
Anstatt wie bisher mit Parametern sollte man nach dem Update die Variablen über Umgebungsvariablen definieren. Symfony stellt eine Komponente bereit, mit der man diese über .env Dateien für die Applikation setzen kann.
Seit Symfony 4.2 gibt es hierfür folgende Dateien:
- .env
- .env.local
- .env.{env}
- .env.{env}.local
Die Dateien werden seitens Symfony in der oben genannten Reihenfolge geladen, wobei die Werte von später geladenen Dateien vorhergehende überschreiben, falls sie bereits gesetzt waren. Dateien, die mit .local enden, sind nur für das lokale System und werden nicht ins Repository committed.
Die .env.local wird bei Testumgebungen nicht geladen. Sollten hier lokale Änderungen notwendig sein, dann muss beispielsweise .env.test.local dafür verwendet werden.
Wenn man aufgrund Symfony 4.0, beziehungsweise 4.1 bereits .env-Dateien hat, dann ist der Inhalt der .env.dist jetzt in .env und von .env in .env.local zu verschieben.
Sollten im Betriebssystem Umgebungsvariablen gesetzt sein, werden diese immer den Werten aus den .env-Dateien des Projekts vorgezogen.
Die Grundidee dazu ist, dass damit in der Entwicklung gearbeitet wird. Dadurch können die Werte einfach geändert werden und auf Produktivsystemen tatsächlich die Umgebungsvariablen setzen.
Im Team haben wir uns dazu entschlossen, auch im Produktivsystem mit den .env-Dateien zu arbeiten:
- Diese sind für uns einfacher zu warten.
- Alle Variablen können an einer Stelle gefunden werden.
- Man befindet sich direkt im Ordner der Applikation.
- Man nicht zuerst herausfinden muss, wo diese im entsprechenden System gesetzt werden.
Zusätzlich können Standardwerte über .env-Dateien ins Repository hinzugefügt werden und nur die notwendigen Werte werden lokal über .env.local beziehungsweise .env.test.local überschrieben.
Mit der .env.local-Datei kann auch festgesetzt werden, um welche Applikationsumgebung es sich handelt. Das hat zur Folge, dass die Symfony Console die richtige Applikationsumgebung verwendet, ohne das extra angeben zu müssen. Zum Beispiel indem man in der Datei APP_ENV=dev ergänzt und die Anwendung im Development Modus laufen lässt.
Sollte man APP_ENV allerdings im System beziehungsweise der Webserverkonfiguration gesetzt haben, dann wird diese gegenüber dem Wert in den .env-Dateien bevorzugt.
Dies gilt auch dann, wenn das Argument --env mit der Symfony Konsole verwendet wird – insofern hier eine aktuelle Verison für bin/console verwendet wird. Anstatt --env kann auch APP_ENV=dev bin/console verwendet werden.
Diese Umgebungsvariablen können auch kompliziertere Informationen beinhalten als nur Strings, wie z.B. JSON, Integer, etc.
Hierzu muss man das nur beim Verwenden der Werte in der Symfony Konfiguration entsprechend definieren. Weitere Information dazu befinden sich in dieser Symfony Dokumentation.
ACHTUNG: Mit Symfony 4.0 und 4.1 war das Handling der .env-Dateien (und deren Verwendung mit bin/console) noch anders. Allerdings sollte man bei einem Upgrade von 3.x ohnehin gleich auf die aktuellste Version upgraden.
Näheres hierzu findet man hier.
Symfony Unit / Functional Tests und Variablen
Damit die Variablen aus .env-Dateien auch in Unit-Tests zur Verfügung stehen, muss phpunit.xml.dist oder phpunit.xml im phpunit Tag folgendes Attribut gesetzt werden:
bootstrap="config/bootstrap.php"
Damit werden die Variablen, wie im Punkt zuvor, auch bei Unit-Tests verwendet und man muss nicht alle Umgebungsvariablen in der phpunit.xml.dist oder phpunit.xml ergänzen.
Hat man in dieser Datei auch den APP_KERNEL Parameter definiert, muss man diese auf
<server name="KERNEL_CLASS" value="App\Kernel" />
anpassen.
Man kann diesen Eintrag auch ganz entfernen, da die Klasse, wie vom Symfony Flex Recipe vordefiniert, über .env.test festgelegt wird.
Nur für die Testumgebung können relevante Variablen (bzw. Änderungen) in der Datei .env.test definiert beziehungsweise in der .env.test.local für das eigene System überschrieben werden. Dank der Neuerung in Symfony 4.2 müssen nicht mehr alle Variablen aus der .env rüberkopiert werden, wie es noch in Symfony 4.0 und 4.1 der Fall war.
ACHTUNG: Diese ausgelagerte Konfiguration config/bootstrap.php für .env-Dateien gab es mit Symfony 4.0 und 4.1 noch nicht. Da musste man diese noch manuell erstellen.
Namespace für Klassen
Das AppBundle wurde entfernt, womit jetzt alle Klassen direkt im src liegen und als Namespace standardmässig App und für Tests App\Tests verwendet wird. Für dieses Umbenennen bzw. gleich das Verschieben wird am besten die Funktion der IDE genommen. Wenn die Applikation eine gute Testabdeckung hat, lässt sich danach einfach feststellen, ob das Umbenennen Probleme verursacht hat oder nicht.
Wenn man wie in Abschnitt "Grundsätzliches Vorgehen" zum Upgrade von Symfony 3.x auf >= 4.2 vorgegangen ist, wurden die Bundles nach src-bundles verschoben. Damit wurde auch das frühere Standard-Bundle AppBundle in src-bundles/AppBundle verschoben.
![Abbildung: PhpStorm – Ansicht sieht bei anderen IDEs entsprechend anders aus. Symphony 4 & Flex](/uploads/media/700x/05/2155-massive-art-Symfony-4-flex.png?v=1-0)
Dann gibt man das Target destination directory an (also den absoluten Pfad zum src-Verzeichnis) und beim New Namespace name gibt man App an. Danach Refactor und bei der Auswahl Select All und OK. So bekommt man noch einmal eine Übersicht der Änderungen und kann jetzt auswählen, ob es welche gibt, die man nicht möchte. Nachdem man dies erledigt hat, klickt man auf Do Refactor und das war’s.
Wenn das Symfony PhpStorm Plugin installiert ist, wird sogar die Konfiguration grossteils automatisch angepasst. Vermutlich müssen ein paar textuelle Vorkommnisse etc. von den Klassen noch korrigiert werden, aber den Großteil hat PhpStorm bereits erledigt. Dasselbe Prinzip kann man auch für den Namespace der Tests verwenden.
Wenn man weitere Bundles hat, sollte man diese strukturiert in den App-Namespace verschieben. Falls das zu viel Aufwand ist, können diese im src-bundles Ordern beibehalten und müssen wie im Abschnitt "Bundles" beschrieben geladen werden. Allerdings sollte man versuchen diese
- in die neue Struktur zu bekommen, oder
- falls die Bundles öfter verwendet werden, über ein eigenes Repository und Composer installieren.
ACHTUNG: Nach dem Umbennen sind auch die autoload Einträge im composer.json eventuell anzupassen.
Applikationsspezifische Konfiguration und Compiler Passes
Nachdem es in der neuen Struktur kein AppBundle mehr gibt, werden CompilerPasses und Konfigurations-Extensions jetzt direkt im Kernel (App\Kernel) definiert. Man ergänzt zuerst die Methode build und verwendet anschliessend $container->addCompilerPass beziehungsweise $container->registerExtension
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new CustomPass());
}
In derselben Methode lassen sich auch eigene Konfigurations-Extensions (zum Erweitern der Konfiguration) definieren:
protected function build(ContainerBuilder $container): void
{
$container->registerExtension(new CustomExtension());
}
Custom Mappings für Doctrine Entities
Nachdem es kein AppBundle mehr gibt, werden Custom Mappings für Doctrine Entities nicht mehr, wie bereits gehabt, automatisch ausgelesen. Stattdessen muss dies entsprechend dieser Anleitung definiert werden. Zum Beispiel wie folgt:
doctrine:
orm:
auto_mapping: true
mappings:
App\Entity:
type: xml
dir: '%kernel.project_dir%/src/Resources/config/doctrine'
prefix: App\Entity
Allerdings wäre es am besten, die Entities in App\Entity zu platzieren und über Annotations zu arbeiten. Hierzu wird standardmässig über Symfony Flex in packages/doctrine.yaml folgende Konfiguration definiert:
doctrine:
orm:
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
Templates
Für Templates gibt es jetzt einen eigenen Ordner templates/ und Templates von verwendeten Bundles können über templates/bundles/ direkt überschrieben werden. Zum Beispiel: templates/bundles/TwigBundle/Exception/error404.html.twig, um die 404-Fehlerseite zu überschreiben.
Es kann jedoch sein, dass dies bei manchen der existierenden Bundles noch nicht funktioniert. Der Grund dafür ist, dass diese die Templates über eine eigene Logik laden, welche die neuen Ordner noch nicht richtig berücksichtigen. Dies sollte aber mit Updates dieser Bundles behoben werden.
Fazit
Ich finde die Änderungen gut, da sie für mehr Übersichtlichkeit sorgen. Ja, es war auch vorher schon möglich, die Konfiguration mehr aufzuteilen. Allerdings ist es jetzt einfacher und bei neu installierten Bundles standardmässig der Fall.
Es ist jedoch schon mit einem gewissen Aufwand verbunden, die Struktur bei einem existierenden Projekt entsprechend anzupassen. Bei einem neuen Projekt bietet es sich natürlich an, gleich mit der neuen Struktur zu starten. Ausserdem empfehle ich, auf Symfony 4.2 oder noch neuer zu aktualisieren, da hier einige Probleme bezüglich Umgebungsvariablen etc. bereits gelöst wurden.
![MASSIVE ART_Andreas Allacher_640x640_circle MASSIVE ART_Andreas Allacher_640x640_circle](/uploads/media/320x320/08/1528-MASSIVE%20ART_Andreas%20Allacher_640x640_circle.png?v=1-0)