mardi 16 janvier 2018

Migration Symfony 3.4 vers 4.0

Quand j'ai commencé à développer le site phpbenchmarks.com, la dernière version de Symfony était la 3.3.
Depuis, la 4.0 est sortie, apportant énormément de changements dans l'architecture du code et du projet.
J'ai effectué cette migration en 3h, pour un site très simple, qui ne contient que 6 pages, sans code très complexe.

Installation de Symfony 4.0


J'ai décidé d'installer un Symfony 4 à côté du projet, pour voir l'architecture, le composer.json etc.
Ensuite, j'ai copié ce qui m'intéressait dans mon projet : les dépendances dans composer.json, le répertoire /config, le contenu de /public/index.php etc.
Là, premier bon point : Symfony est très léger par défaut !
Les répertoires sont plus cohérents. Je n'avais jamais compris pourquoi /app et /src étaient séparés, avec le Kernel dans /app.
On ne savait pas trop si on faisait un seul bundle dans /src ou plusieurs, le débat étant sans fin.
Maintenant, c'est clair : plus de bundle dans /src, suppression du répertoire /app (tout le code PHP est dans /src), le répertoire /config est à la racine et contient un fichier par bundle etc.

Installation des dépendances


Au final, on se rend vite compte que le Symfony installé de base ne suffira pas pour notre projet. C'est bien là le but de Flex : installer des dépendances très facilement, et surtout, que quand on en a besoin ! Ca va nous éviter d'avoir le composant LDAP installé par exemple, alors qu'on ne s'en sert pas.
Pour ça, Flex fait très bien son boulot. Un simple composer require translator pour installer et configurer symfony/translator, sans se poser de questions et lire une doc compliquée. On peut très vite voir les fichiers ajoutés ou modifiés par cette dépendance. Top !

Ce qui l'est moins, à mon sens, c'est qu'une dépendance souvent basique en a d'autres derrière, et qu'on se retrouve très vite à installer énormément de composants de Symfony.
Au final, on se retrouve quasiment avec un Symfony full stack, avec seulement quelques dépendances en moins. J'aurais bien aimé ne pas avoir le translator installé par quasiment toutes les dépendances par exemple.

Suppression des bundles


Le débat pas de bundle / AppBundle / un bundle par "section" du site est enfin terminé : pas de bundle a gagné !
On retrouve quasiment la structure interne d'un bundle, directement dans /src : /Controller, /EventListener etc.
Après avoir déplacé tous ces fichiers, il va falloir renommer tous les use PHP (attention au regroupement des use depuis PHP 7.1, une simple recherche ne retrouve pas toujours tout), les appels à constant() dans Twig (attention au double \\ dans la recherche) etc.

Suppression des bundles (2)


Avec la suppression des bundles, que se passe-t-il avec tous les fichiers qui étaient automatiquement inclus et lus par Symfony 3.4 ? Et bin, rien. On doit tout définir manuellement.

Par exemple, j'avais une configuration pour changer le paramètre router.options.generator_base_class (déprécié en 3.3, supprimé en 4.0 donc), pour utiliser mon générateur d'url.
J'ai du passer par un CompilerPass, définit manuellement (le répertoire /src/DependencyInjection n'est pas lu automatiquement, comme pour un Bundle), dont voici le code :
class UrlGeneratorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $definition = $container->getDefinition('router.default'); $options = $definition->getArgument(2); $options['generator_class'] = UrlGenerator::class; $options['generator_base_class'] = UrlGenerator::class; $definition->setArgument(2, $options); $container->setDefinition('router.default', $definition); } }
Et l'intégration dans Symfony 4, dans /src/Kernel.php :
protected function buildContainer(): ContainerInterface { $container = parent::buildContainer(); $container->addCompilerPass(new UrlGeneratorPass()); return $container; }


Le routing


Premier gros changement : le suffix Action dans le nom des méthodes des Controller n'est plus automatiquement géré. Donc soit vous supprimez tous les suffixes Action dans le nom de vos méthodes, soit vous ajoutez Action dans toutes les configurations de route _controller (qui a un peu changé aussi). Ca peut prendre pas mal de temps, les dépendances externes peuvent ne pas avoir encore effectué la modification, etc.

Dans vos configurations de route, une configuration controller a été ajoutée, pour remplacer defaults: {_controller: FooController}, qui n'était pas très intuitive.
foo: path: /foo methods: GET controller: App\Controller\FooController::bar

La syntaxe est devenue très précise : ce n'est pas la même si le Controller est un service ou non.
Si c'est un service : controller: service:index, sinon : controlller: App\Controller\FooController::index.
Notez le subtil : simple pour indiquer que c'est un service, de nom service, et le :: double, pour indiquer que ce n'est pas un service, et qu'indique le FQCN du Controller.

Les Controller en service


Par défaut, la configuration définit tous ce qui est dans /src/Controller comme étant un service (via l'autowiring), dont l'id est le FQCN du Controller.
J'ai voulu essayer, parceque ça me parait bien mieux que d'avoir une dépendance vers le Container dans les Controller.
Finalement, comme mes actions ont des dépendances très différentes, je me suis vite retrouvé avec un __contruct() qui contient trop d'arguments inutiles pour l'action en cours.
De plus, si notre Controller implémente ContainerAwareInterface ou étend de AbstractController, le container sera injecté dans notre Controller (voir ControllerResolver). Logique pour l'interface, mais pas intuitif pour AbstractController. Vu que la majorité des Controller étendent d'AbstractController, on se retrouve au final avec des dépendances précises dans __construct(), mais également le Container, ce qui fait doublon et n'est pas forcément le comportement voulu au départ.

Au final, j'ai supprimé l'autowiring pour les Controller.

L'autowiring


La configuration par défaut définit tous ce qui est dans /src comme étant un service, et va chercher les dépendances automatiquement dans la signature de la méthode __construct si elle existe.
On peut se dire que c'est génial, qu'on ne touchera plus à un /Resources/config/service.yml de notre vie !

La réalité n'est pas si évidente : seule la signature de __construct sera gérée automatiquement, si le typage est bien fait, et que les arguments sont des objets. Oubliez les types scalaires, ils ne peuvent pas être gérés automatiquement.

Donc tous les appels qu'on faisait par calls dans services.yml ne sont pas gérés.
La syntaxe @=service('doctrine').getRepository('App\Entity\Foo') n'est pas gérée.
Les paramètres ne sont pas gérés (%kernel.root_dir% par exemple).

Pour ma part, j'ai vite déchanté, vu que j'utilise régulièrement ces configurations. Donc j'ai enlevé l'autowiring, qui ne me servait que très peu, pour conserver ma configuration manuelle dans /config/services.yaml.

Autre problème de l'autowiring : l'identifiant du service devient le FQCN. On ne peut pas en spécifier un manuellement, indiquer un préfixe, ou un Formatter qui créerait un identifiant de service suivant certaines règles. Du coup, tout le principe d'identifiant pour encapsuler le FQCN, et ne changer qu'un fichier de config en cas de renommage d'une classe n'est plus utilisé : vous changez un nom de classe, vous changez tous les appels à ce service !
De plus, je ne suis pas fan des configurations en 4 lignes, qui vont parser tout mon code, et peut-être faire 99% de bonnes choses, mais 1% de bétises qui vont bien nous faire galérer.

Doctrine


Quand on veut passer un Repository en dépendance à un service, on peut utiliser cette syntaxe :
@=service('doctrine').getRepository('FooBundle:BarEntity')
Elle n'est plus intégrée par défaut, il faut installer la dépendance symfony/expression-language.
De plus, avec la suppression des bundles, la syntaxe n'a plus lieu d'être, elle doit être remplacée par :
@=service('doctrine').getRepository('App:BarEntity')
Par défaut, l'installation de Doctrine dans Symfony 4 est configurée pour que les entités soient mappées via des annotations. J'utilise du yml (les .orm.yml sont dans /config/doctrine), dont voici la configuration dans /config/packages/doctrine.yml :
doctrine: orm: mappings: App: is_bundle: false type: yml dir: '%kernel.project_dir%/config/doctrine' prefix: 'App\Entity' alias: App
La configuration par défaut pour le nommage des champs a été changée. Par défaut, le champ SQL pour une propriété fooBar d'une entité était fooBar. Maintenant, c'est foo_bar.
Pour revenir à l'ancien nommage, il faut supprimer cette configuration :
doctrine: orm: naming_strategy: underscore
Plus de /app/config/parameters.yml pour la configuration de l'accès à la base de données, tout se passe via une varaible d'environnement : DATABASE_URL. Voir la section Virtual Host plus bas pour un exemple.

Traductions


Les fichiers de traductions sont dans /translations. Pratique pour les traducteurs, si on veut leur partager un répertoire, sans leur expliquer où aller fouiller dans notre projet !

Templates


Les templates sont dans /templates. Pratique pour les intégrateurs, si on veut leur partager un répertoire, sans leur expliquer où aller fouiller dans notre projet !

Attention cependant, la syntaxe pour utiliser un template a changé :
$this->render('FooBundle:Controller:template.html.twig');
devient
$this->render('Controller/template.html.twig');
avec template.html.twig dans /templates/Controller.

Virtual host


Pour nginx, rien de bien compliqué : il faut simplement changer le répertoire /web par /public, et appeler index.php, quel que soit l'environnement (plus de app.php, app_dev.php etc).
Exemple de Virtual Host :
server { listen 80; server_name phpbenchmarks.loc; root /var/www/mysite/public; location / { try_files $uri /index.php$is_args$args; } location ~ ^/index.php(/|$) { fastcgi_pass unix:/run/php/php7.2-fpm.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param HTTPS off; fastcgi_param DATABASE_URL 'mysql://USER:PASSWORD@127.0.0.1:3306/DATABASE'; } error_log /var/log/nginx/mysite_error.log; access_log /var/log/nginx/mysite_access.log; }


PhpStorm


Le plugin Symfony pour PhpStorm ne retrouve pas les services définit dans /config/services.yaml.
Un peu déroutant, on perd toute l'auto-complétion sur nos services, et les appels à $container->get('service') sont tous indiqués en erreur !

Il faut changer les configuration suivantes pour le plugin :
Path to UrlGenerator : var/cache/dev/srcDevDebugProjectContainerUrlGenerator.php Web directory : public

J'utilise encore la version 2016 de PHP Storm, c'est possible que la version 2018 corrige quelque détails.

Et la performance dans tout ça ?


La migration finalisée pour phpbenchmarks.com, j'ai forcément fait un petit benchmark.
Et là, surprise : 0 différence, pas même 1ms. Si on se réfère aux benchmarks de phpbenchmarks, Hello World a un gain énorme entre la 3.4 et la 4.0, mais pas API Rest.

Ca se traduit bien sur mon site : vu que j'ai du installer énormément de dépendances (la plupart encapsulées dans d'autres dépendances), au final, j'ai un framework quasiment complet installé, donc pas de gain grâce à Flex.


Conclusion

  • Le passage de Symfony 3.4 à 4.0 n'est pas aussi simple que de la 2.8 à la 3.0.
  • Les changements de répertoire sont une très bonne chose, c'est bien plus carré et plus séparé. Cependant, ça peut demander beaucoup de temps, pour renommer tous les namespace, les use, les appels dans Twig, etc.
  • L'installation de dépendances via Flex est très simple, bien documentée, c'est top !
  • Je pensais avoir un gain de performance entre la 3.4 et la 4.0, de ce côté je suis un peu déçu.
  • Je pense que Symfony 4.0 a instauré une nouvelle base, un réel découpage en composants (c'était déjà le cas, mais qui a déjà installé un Symfony composant par composant, manuellement ?), ce qui ne peut qu'être salué !
  • La migration e m'a pris que 3h, je pensais en avoir pour bien plus longtemps que ça.

Aucun commentaire:

Publier un commentaire