Von Symfony 3 zu 4.4 zu 5.1

Du möchtest Symfony endlich mal updaten? Es lohnt sich! Zuvor möchte ich dir aber mal gratulieren. Das wird dir Spaß machen!

Aber mal im Ernst: Seinen Source-Code immer aktuell zu halten, hilft, die neuesten Features ohne Kopfschmerzen nutzen zu können. Symfony 4 bietet so viel neues, von Symfony 5 ganz abgesehen.

Aber wenn du noch auf Symfony 3 rumdümpelst, steht dir eine abenteuerreiche Reise bevor. Stelle dir Frodos Spaziergang nach Mordor vor, nur mit 100kg Betonschuhen an den Füßen und einem Eimer auf dem Kopf.

Oder doch nicht so schlimm? Auf meiner eigenen Reise sind mir einige Fehler begegnet, die dich vielleicht auch aufhalten. Und damit du wenigstens zwei Löcher für die Augen im Eimer hast, schreibe ich dir die (möglichen) Lösungen direkt dazu.

Schön ist das nicht!

Manche Lösungen sind nicht schön und auch gar nicht, wie Symfony 4/5 sich das vorstellt. Es gibt eine neue Struktur namens Flex, die auch wunderbar ist; allerdings nur dann, wenn man in der Praxis so viel Zeit hat, alles bilderbuchtreu umzusetzen.

Wer hat das schon! Also drücken wir mal ein Auge zu und konzentrieren uns darauf, den Quellcode in der alten Ordnerstruktur zu belassen und so wenige Anpassungen wie möglich zu machen. Hauptsache datt Dingen lüppt; schöner machen kann man hinterher immer noch (haha).

Trotzdem ein Tipp auf dem Weg: Installiere dir eine frische Version Symfony 5 und arbeite dich in die neue Struktur ein. Vieles ist einfacher geworden – doch du musst es nicht nutzen, wenn du nicht willst. Es sei denn, du wirst gezwungen.

Das Schwierigste: Assetic durch Encore/Webpack austauschen

Assetic gibt es in Symfony 4 nicht mehr; es wurde ersetzt durch webpack, bzw. Encore, einer Symfony Komponente, die hilft, mit webpack umzugehen (da es wohl doch nicht so ganz trivial ist).

Nach einigen Monaten in der Nutzung von webpack muss ich sagen: Ja, das lohnt sich. Wenn du die Grundzüge kennenlernen willst, schau in meinen Blog-Beitrag rein!

Wenn du das gemacht hast, update Symfony auf 3.4 (falls noch nicht geschehen); das Backwards Compatibility Promise sorgt dafür, dass dein Code noch läuft, aber schon Deprecation Notices erhält.

Deprecation Notices abarbeiten

Augen zu und durch; wie der Name schon sagt: Arbeite alle Deprecation Notices ab. Das kann durchaus vierstellig sein, aber vieles wiederholt sich. Wenn eine Notice nicht aus deinem Code kommt: Das klärt sich, wenn du Symfony updatest und einfach wie Rambo in den Ein-Personen-Krieg ziehst.

Auf Symfony 4.4 updaten

Nach der erfolgreichen Bekämpfung der (meisten) Deprecations kannst du den Schritt wagen und auf 4.4 updaten. Dazu solltest du folgende Anpassungen in deiner composer.json vornehmen:

  • require > php > 7.4
  • config > platform > php > 7.4
  • Alle Symfony-Packages, die auf 3.4.* stehen, auf 4.4.* ändern
  • Prüfe auch deine anderen Komponenten, ob diese einen Major-Version-Sprung hatten, bzw. ob du eine Version aus DDR-Zeiten verwendest

Anschließend solltest du folgende Packages entfernen (falls die bei dir noch vorhanden sind):

  • sensio/distribution-bundle
  • sensio/generator-bundle

Dann die Packages updaten:

composer update

Manchmal möchtest du alle deine Pakete zurücksetzen, um einen “sauberen” Start zu haben. Kein Problem: Lösche einfach das Verzeichnis vendor und führe composer install aus. Deine Pakete werden dann gemäß der composer.lock Datei, die du vorher geupdatet hast, wieder frisch geladen.

Fehler: Attempted to call an undefined method named “loadClassCache” of class “AppKernel”

Kommentiere in web/app.php und web/app_dev.php die Zeile $kernel->loadClassCache(); aus. Die gibt es schon seit Symfony 3.3 nicht mehr.

Fehler: Attempted to load class “SensioDistributionBundle” from namespace…

Das SensioDistributionBundle gibt es nicht mehr – es ist durch Symfony 4 obsolet geworden. Ersetze deshalb in der Datei AppKernel.php das SensioDistributionBundle durch SensioGeneratorBundle. Bzw. tue das nicht, wenn du die Funktionalitäten dafür nicht benötigst.

Twig updaten

Sollte dein Twig noch auf Version 2 oder 3 sein, bearbeite die folgenden Komponenten in composer.json:

# Entfernen:
- twig/twig

# Hinzufügen 
- "symfony/twig-bundle": "4.4.*"

Nicht vergessen, ein composer update durchzuführen.

Service templating entfernen

In der config.yml löschst du folgende Zeilen:

framework:
    # templating:
    #     engines: ['twig']

Das liegt daran, dass der Service templating nicht mehr existiert. In Symfony 4.4 ist das zwar nur deprecated, hilft dir aber später.

In deinem Quellcode ersetzt du einfach alle Aufrufe von ->get("template") durch ->get("twig").

Bundle-Pfade umschreiben

Symfony 4 kennt eigentlich keine Bundles mehr, deshalb gibt es jetzt auch eine andere Schreibweise für die Referenzierung der Templates; die müssen alle in deinem Quellcode angepasst werden. Ja, das ist Freude.

Alt: MeinTollesBundle:MeinController:meinTemplate.html.twig
Neu: @MeinTolles/MeinController/meinTemplate.html.twig

Maker-Bundle hinzufügen

Falls du Doctrine-Migrations nutzt: Das ist jetzt alles anders. Früher ging das bei mir so:

# Entity-PHP-Dateien aus Schema erstellen
php bin/console doctrine:generate:entities MeinTollesBundle --no-backup

# Migration-Datei erstellen
php bin/console doctrine:migrations:diff

# Migrations ausführen
php bin/console doctrine:migrations:migrate

Ich nutze Skipper als Datenbank-Design-Tool, welches mir auf Knopfdruck die Schemadateien exportiert. Und da meine Entity-Files keinen zusätzlichen Schabernack beinhalten, werden die einfach jedesmal neu generiert. Eine Änderung der Datenbank ist damit in Sekunden erledigt.

Die neuen Befehle sind wie folgt:

# Entity-PHP-Dateien aus Schema erstellen
php bin/console make:entity --regenerate 

# Migration-Datei erstellen
php bin/console make:migration

# Migrations ausführen
php bin/console doctrine:migrations:migrate

Migrations erledigt jetzt also das maker-bundle. Um das ohne Flex zu installieren, machst du folgendes:

composer require symfony/maker-bundle

# In AppKernel.php
$bundles[] = new Symfony\Bundle\MakerBundle\MakerBundle();

# In config.yml
maker:
    root_namespace: 'MeinTollesBundle'

Anschließend hast du den Workflow auch wieder im Griff.

Commands wieder aktivieren

Es kann sein, dass deine Commands nicht mehr erkannt werden. Diese musst du jetzt autowiren. Klingt alles schlimmer als es ist: Grundsätzlich gibst du in der services.yml nur an, wo Symfony nach Klassen für einen bestimmten Namespace suchen soll und welche Services oder Dependencies injiziert werden. Das sieht dann z.B. so aus:

# services.yml des Bundles
services:
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
 
    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    MeinTollesBundle\Command\:
        resource: '%kernel.root_dir%/../src/MeinTollesBundle/Command/*'

Und schon läufts wieder!

Controller neu ableiten

Vermutlich werden alle deine Controller von Symfony\Bundle\FrameworkBundle\Controller\Controller abgeleitet. Tja, Pech, das gibt’s bald nicht mehr. Deshalb führe ein Search&Replace aus und ersetze es durch AbstractController.

Das hat ein paar Nachteile (bzw. Vorteile, wenn man diese verstanden hat). Eines davon ist folgender Fehler:

Fehler: “\MeinTollesBundle\Controller\MeinController” has no container set, did you forget to define it as a service subscriber?

Auch hier musst du (wie bei den Commands) autowiring aktivieren:

# services.yml deines Bundles
services:
    _defaults:
        autowire: true  
        autoconfigure: true 

    MeinTollesBundle\Controller\:
        resource: '%kernel.project_dir%/src/MeinTollesBundle/Controller/*'
        calls:
            - method: setContainer
              arguments: ['@service_container']

Die unteren Anweisungen helfen gegen o.a. Fehler, so dass du deine eigenen Services wieder über $this->get("meinservice") benutzen kannst. Aber Achtung: Das solltest du eigentlich nicht tun – stattdessen packst du deine Service-Klassen direkt in die Action deines Controllers.

Fehler: The “MeinTollesBundle\Controller\MeinController::getParameter()” method is missing a parameter bag to work properly. Did you forget to register your controller as a service subscriber?

Tja, getParameter() ist nicht mehr im AbstractController verfügbar. Der Hack dazu: Ersetze alle $this->getParameter() durch $this->container->getParameter().

Achtung: Auch das ist nicht schön. Nutze stattdessen Bindings.

Update auf 5.1

Jetzt bist du soweit: Ändere alle Paketversionen von 4.4.* auf 5.1.*! Anschließend können dir folgende Fehler begegnen.

Fehler: PHP Fatal error: Uncaught Error: Class ‘Symfony\Component\Debug\Debug’ not found in /home/swoplo/projects/portal/bin/console

Ganz einfach: Ändere die Klasse Symfony\Component\Debug\Debug in Symfony\Component\ErrorHandler\Debug. Dieser Fehler kann dir auch in der app.php und app_dev.php begegnen. Gleiche diese am besten mit der aktuellen index.php-Version ab.

Fehler: You have requested a non-existent parameter “kernel.root_dir”. Did you mean one of these: “kernel.project_dir”, “kernel.cache_dir”, “kernel.logs_dir”?

kernel.root_dir gibt es nicht mehr; das hat immer auf /app gezeigt. Deshalb machst du jetzt folgendes:

  • '%kernel.root_dir%/../' ersetzen durch '%kernel.project_dir%/'
  • '%kernel.root_dir%/PFAD/' ersetzen durch '%kernel.project_dir%/app/PFAD'
Fehler: Too few arguments to function Symfony\Component\Config\Definition\Builder\TreeBuilder::__construct()

Das ist auch einfach: Schreibe einfach die Anweisungen in deinen Configurations nach dieser Anleitung um.

Fehler: Class MeinBundle\Twig\AppTranslationExtension may not inherit from final class (Symfony\Bridge\Twig\Extension\TranslationExtension)”

Falls du die Twig-Translation-Klasse selbst angepasst hast: Das geht jetzt nicht mehr; die frühere Vaterklasse ist jetzt final. Lösung: Kopiere die Vaterklasse und pflege dort deine Anpassungen ein.

Fehler: Unable to find file “@TwigBundle/Resources/config/routing/errors.xml”

Das Twig-Bundle gibt es nicht mehr, auch hier ist die Anpassung nicht schwer. Tausche folgendes aus:

# In routing_dev.yml
resource: '@TwigBundle/Resources/config/routing/errors.xml'

# ersetzen durch:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
Fehler: Argument 1 passed to MeinTollesBundle\EventListener\ExceptionListener::onKernelException() must be an instance of Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent, instance of Symfony\Component\HttpKernel\Event\ExceptionEvent given

Auch die Listener haben sich geändert; es gab ein paar nötige Namensanpassungen. In diesem Fall ersetzt du \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent einfach durch \Symfony\Component\HttpKernel\Event\RequestEvent. Analog musst du möglicherweise deine anderen Event-Klassen anpassen.

Fehler: Return value of “MeinTollesBundle\Command\Script\MeinCommand::execute()” must be of the type int, “null” returned

In Symfony 4.4 wurde beschlossen, dass Command::execute() ein Integer zurückgeben muss. Deshalb musst du jetzt alle Commands beenden mit: return 0; bzw. alle return; anpassen (oder mit einem Fehler-Integer versehen).

Abschlusswort

Es gibt kein schöneres Gefühl, als seine Software in der neuesten Framework-Version lauffähig zu machen. Doch, gibt es. Aber das ist an dieser Stelle egal, denn du hast es hoffentlich geschafft! Drucke dir nun folgenden Text auf dein T-Shirt:

Symfony victus sum.

Das sollte richtig sein. So ungefähr. OK; ist vermutlich total falsch. Aber meine letzte Latein-Klausur ist 25 Jahre her – wer weiß, ob es bei dieser Sprache auch schon wieder Updates gab.

Björn Falszewski
9. September 2020
Disclaimer
Alle meine Artikel entstehen mit bestem Wissen und Gewissen, sind aber nicht perfekt und sollten immer nur als Ausgangspunkt für deine eigenen Recherchen bilden.

Sollte dir etwas Fehlerhaftes auffallen, freue ich mich über deine Nachricht!