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.