Jedes Symfony-Projekt wird früher oder später auf die neue Symfony-Flex-Struktur angepasst. Hier und da kann einem die vorgegebene Upgradeanleitung in der Dokumentation etwas zu dünn erscheinen. Oftmals kommt es auf die Details an, die einem den Umstieg auf die neue Symfony-Flex-Struktur erleichtern können.
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.
Bevor man mit dem Upgrade von Symfony 3.x auf >= 4.2 beginnt, sollte man folgende Dateien (sofern vorhanden) sichern:
.env
.env.dist
bin/console
weitere modifizierte oder selbst erstellte Dateien im Ordner bin
Nach der Aktualisierung der Version 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. Außerdem sollten mögliche Änderungen an der bin/console von Beginn an notiert werden, damit diese nach einem Update wiederhergestellt werden können.
Zusätzlich sollte das Verzeichnis src vor dem Update in src-bundles umbenannt werden, denn 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.
Um Symfony zu aktualisieren, sind die folgenden Änderungen in composer.json durchzuführen:
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. Dazu zählen
das Entfernen von incenteev/composer-parameter-handler in require und require-dev-Sektionen,
das Entfernen aller Vorkommnisse von Incenteev\\ParameterHandler\\ScriptHandler::buildParameters in der scripts-Sektion und
das Entfernen der alten scripts-Einträge, die mit Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler:: beginnen; hier kommt stattdessen auto-scripts zum Einsatz.
Neue Einträge in scripts erstellen (Listing 1); bei diesem Schritt sind eventuell eigene Skripte zu ergänzen. Nach dem Upgrade von Symfony sieht das dann z. B. wie in Listing 2 aus. Hiermit kann man in auto-scripts weitere Symfony-Befehle ähnlich wie diese ergänzen bzw. bei assets:install definieren, ob man Kopien der Dateien oder Symlinks verwenden will.
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 die Dokumentation halten [1].
Zusätzlich sollte man die Inhalte aus Listing 3 in der extra-Sektion ergänzen:
require definiert hierbei, welche Version für die Symfony-Komponenten zum Einsatz kommt.
allow-contrib definiert, dass Symfony Flex Recipes nicht nur für die offiziellen Symfony-Komponenten, sondern auch für solche anderer Bundles verwendet werden (eine Übersicht, welche das sind, findet man auf GitHub [2]). Ist dies nicht gewünscht, muss hier false eingesetzt werden (Listing 3). Es empfiehlt sich, diesen Wert zumindest bis nach dem Upgrade von Symfony auf true zu setzen und nach Bedarf danach auf false. Damit erhält man zumindest die Grundkonfiguration für die meisten der installierten Bundles.
Aufgrund der extra-Sektion lassen sich die Symfony Dependencies jetzt wie in Listing 4 in require hinzufügen. Diese Liste kann bei Bedarf um weitere Dependencies erweitert werden – je nachdem, ob das für das Projekt notwendig beziehungsweise gewollt ist.
Um sicherzustellen, dass die komplette Symfony Library nach der Änderung nicht mehr zum Einsatz kommt, muss Folgendes ergänzt werden:
"conflict": {
"symfony/symfony": "*"
}
Falls es conflict schon gibt, muss lediglich "symfony/symfony": "*" ergänzt werden.
Aus autoload muss der folgende Eintrag entfernt werden: "classmap": ["app/AppKernel.php", "app/AppCache.php"]
Im autoload unter psr-4 muss der folgende Eintrag ergänzt werden: App\\": "src/",
Existierende autoload-Einträge müssen entsprechend von src auf src-bundles angepasst werden, beispielsweise: "AppBundle\\": "src-bundles/AppBundle/",
Listing 1
"scripts": {
"auto-scripts": [
],
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
Listing 2
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
Listing 3
"symfony": {
“allow-contrib": true,
"require": "4.2.*"
}
Listing 4
"symfony/console": “*",
"symfony/dotenv": "*",
"symfony/flex": "^1.1",
"symfony/framework-bundle": "*",
"symfony/yaml": „*",
Am Schluss sollte ein fertiges composer.json, ähnlich dem des Symfony Skeletons [3], entstanden sein. Symfony Flex Recipes können unter [4] eingesehen oder gesucht werden. Auch auf GitHub in den Repositories [5] und [6] finden sich weitere Symfony Recipes.
Möchte man Symfony updaten, müssen zuerst folgende Verzeichnisse und Dateien gelöscht werden:
bin
vendor
symfony.lock (falls vorhanden)
Im Anschluss kann Symfony mittels Composer aktualisiert werden: COMPOSER_MEMORY_LIMIT=-1 composer update -o. Dabei hängt der Pfad zum Composer 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 Abschnitt „Konfiguration“).
Es kann nicht --no-scripts verwendet werden, da in diesem Fall die Symfony Flex Recipes nicht ausgeführt werden.
COMPOSER_MEMORY_LIMIT=-1 wird verwendet, um zu verhindern, dass der Composer durch das Memory-Limit gestoppt wird. Aufgrund der vielen Änderungen könnte das der Fall sein – falls PHP in der CLI hier nicht bereits unbeschränkt ist.
Alle weiteren Änderungen können der Upgradeanleitung im Symfony Repository [7] entnommen werden. Am wichtigsten ist hier, dass der web-Ordner zu public wird und die app_*.php-Dateien durch eine public/index.php ersetzt werden (diese wurde beim Upgrade auch gleich angelegt), die sich darum kümmert, dass die korrekte Umgebung verwendet wird. Das bedeutet, dass die Webserverkonfigurationen angepasst werden müssen und eventuelle Änderungen an den app_*.php-Dateien in der neuen public/index.php zu übernehmen sind.
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 Abschnitt „Namespace für Klassen“), könnte man hier auch eigene Bundles ergänzen. Die Konfiguration darin sieht folgendermaßen 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 im Beispiel 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.
Vor der Änderung gab es eine Datei im Symfony-Projekt, über die 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 ein Vorschlag zur Vorgehensweise:
Bundles können über Symfony Flex neu installiert werden (siehe Abschnitt „Bundles“), wodurch die Standardkonfiguration erstellt wird. Das hat den Vorteil, dass der für das Bundle vorgesehene Name der Konfigurationsdatei zum Einsatz kommt.
Danach können diese mit den Werten der ursprünglichen Konfiguration angepasst werden. Diese Konfiguration findet man in config/packages/*.yaml beziehungsweise config/packages/{env}/*.yaml.
Parameter für die Anwendung, beispielsweise locale: 'en', sind jetzt standardmäßig in der Datei für die Services definiert.
In unserem Projekt haben wir diese allerdings in config/packages/app.yaml gegeben, damit man sie 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.
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 sie ä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 z. B. auch XML einsetzen.
Man kann hier auch über Glob-Patterns weitere Dateien oder auch komplette Ordner laden:
imports:
- { resource: "services/*.{php,xml,yaml,yml}" }
# oder auch
imports:
- { resource: "services/**/*.{php,xml,yaml,yml}" }
So kann man die Services weiter aufteilen und sich Dank Autoconfigure und Autowiring die Konfiguration der Services oft ganz oder fast komplett sparen:
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.
Anstatt wie bisher mit Parametern sollte man nach dem Update die Variablen über Umgebungsvariablen definieren. Symfony stellt eine Komponente bereit, mit der man diese mittels .env-Dateien für die Applikation setzen kann.
Seit Symfony 4.2 gibt es hierfür Dateien, die von Symfony in der in folgenden Reihenfolge geladen werden, wobei die Werte von später geladenen Dateien vorhergehende überschreiben, falls sie bereits gesetzt waren:
.env
.env.local
.env.{env}
.env.{env}.local
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 von Symfony 4.0 bzw. 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. Denn diese sind für uns einfacher zu warten. Außerdem können alle Variablen an einer Stelle gefunden werden und man befindet sich direkt im Ordner der Applikation. Überdies muss man nicht zuerst herausfinden, 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.
This Whitepaper contains an exclusive selection of PHP knowledge on over 40 pages for you to discover. Read articles from our experts on Laravel 10, CakePHP 5, Shopware 6, Clean Code, Single Page Applications with PHP and much more!
Download it now and learn more about the exciting developments in the PHP world!
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. Bei Verwendung des Arguments --env mit der Symfony Console überschreibt dieses die APP_ENV-Umgebungsvariable – insofern hier eine aktuelle Version 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 usw. Hierzu muss man das nur beim Verwenden der Werte in der Symfony-Konfiguration entsprechend definieren. Weitere Information dazu finden sich in der Symfony-Dokumentation [8].
Wichtig zu beachten ist, dass in Symfony 4.0 und 4.1 das Handling der .env-Dateien (und deren Verwendung mit bin/console) noch anders war. Allerdings sollte man bei einem Update von 3.x ohnehin gleich auf die aktuellste Version upgraden. Näheres hierzu findet man auf dem offiziellen Symfony-Blog [9].
Damit die Variablen aus .env-Dateien auch in Unit-Tests zur Verfügung stehen, muss für 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 diesen 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.
Dabei gilt es zu beachten, dass es die ausgelagerte Konfiguration config/bootstrap.php für .env-Dateien mit Symfony 4.0 und 4.1 noch nicht gab. Da musste man diese noch manuell erstellen.
Das AppBundle wurde entfernt, womit jetzt alle Klassen direkt im src liegen und als Namespace standardmäßig 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“ zu Beginn dieses Artikels vorgegangen ist, wurden die Bundles nach src-bundles verschoben. Damit wurde auch das frühere Standard-Bundle AppBundle in src-bundles/AppBundle verschoben. Jetzt kann dessen Inhalt in src verschoben werden und man kann mit dem Anpassen der Klassennamen beginnen.
Mit PhpStorm [10] kann man das Verschieben des Namespace und der Dateien kombinieren, indem man folgendermaßen vorgeht: Man wählt die AppBundle\AppBundle-Klasse aus, markiert den Namespace (App) und wählt im Kontextmenü Refactor | Move (Abb. 1).
Im Anschluss gibt man das Target destination directory (also den absoluten Pfad zum src-Verzeichnis) an und beim New Namespace name gibt man App an. Danach wählt man 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 ist fertig.
Wenn das Symfony-PhpStorm-Plug-in [11] installiert ist, wird sogar die Konfiguration größtenteils automatisch angepasst. Vermutlich müssen ein paar textuelle Vorkommnisse etc. der Klassen korrigiert werden, aber das meiste 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. Doch Vorsicht: Nach dem Umbenennen sind auch die autoload-Einträge in composer.json eventuell anzupassen.
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 anschließend
$container->addCompilerPass
beziehungsweise
$container->registerExtension
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new CustomPass());
}
In derselben Methode lassen sich auch eigene Konfigurations-Extensions, beispielsweise zum Erweitern der Konfiguration, definieren:
protected function build(ContainerBuilder $container): void
{
$container->registerExtension(new CustomExtension());
}
Weil es kein AppBundle mehr gibt, werden Custom Mappings für Doctrine Entities nicht mehr, wie bereits gehabt, automatisch ausgelesen. Stattdessen muss dies entsprechend einer Anleitung [12] definiert werden. Zum Beispiel wie in Listing 5.
Listing 5
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äßig über Symfony Flex in packages/doctrine.yaml eine Konfiguration definiert (Listing 6).
Listing 6
doctrine:
orm:
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
Für Templates gibt es jetzt einen eigenen Ordner templates/ und Templates von verwendeten Bundles können über templates/bundles/ direkt überschrieben werden, wie 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, die die neuen Ordner noch nicht richtig berücksichtigen. Dies sollte aber mit Updates dieser Bundles behoben werden.
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äßig 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. Außerdem empfehle ich, auf Symfony 4.2 oder noch neuer zu aktualisieren, da hier einige Probleme bezüglich Umgebungsvariablen etc. bereits gelöst wurden.
Andreas Allacher ist Full Stack Web Developer bei MASSIVE ART in Wien. Der studierte Physiker widmet sich im Arbeitsalltag anspruchsvollen Software- und E-Commerce-Projekten. In seiner Freizeit schlägt sein Herz zudem für unkonventionelle Themen wie die Quantenkryptographie oder Richtfunknetzwerke für das Familienweingut.
[1] https://symfony.com/doc/current/configuration/override_dir_structure.html
[2] https://github.com/symfony/recipes-contrib
[3] https://github.com/symfony/skeleton/blob/4.2/composer.json
[5] https://github.com/symfony/recipes
[6] https://github.com/symfony/recipes-contrib
[7] https://github.com/symfony/symfony/blob/master/UPGRADE-4.0.md
[8] https://symfony.com/doc/current/configuration/external_parameters.html
[9] https://symfony.com/blog/new-in-symfony-4-2-define-env-vars-per-environment
[10] https://www.jetbrains.com/phpstorm/
[11] https://github.com/Haehnchen/idea-php-symfony2-plugin
[13] Weiterführende Informationen: Symfony-Dokumentation https://symfony.com/doc/current/index.html