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.

vendredi 1 décembre 2017

PHP 7.2 est là !

Ca y est, PHP 7.2 est disponible !

Voilà une liste des principales nouveautés :
  • Convert numeric keys in object/array casts : correction d'un gros bug dans le cast des clefs d'un tableau, quand on convertit un tableau en objet, et un objet en tableau.
  • Counting of non-countable objects : ajout d'une E_WARNING quand on appelle count() sur un objet qui n'implément pas \Countable. Continue de retourner 1 dans ce cas (qui ne correspond à rien), pour garder la compatibilité.
  • Object typehint : ajout du type hint object, qu'on peut donc utiliser dans la signature d'une méthode par exemple, pour indiquer qu'une variable est un object dont ne connait pas la classe. Il ne manquait quasiment que ça ! Plus que le typage des propriétés, et peut-être des variables, et on sera enfin débarassé de toutes ces PHPDoc ;).
  • Migration Hash Context from Resource to Object : enfin une meilleure gestion des resources, qui sont des objets particuliers.
  • Argon2 Password Hash : ajout de Argon2 comme encryptage disponible.
  • Improved SSL / TLS constants : évolution dans la gestion de TLS, qui privilégie la sécurité à la compatibilité entre les versions de PHP.
  • Deprecate (then Remove) Mcrypt : mcrypt, librairie d'encryptage qui était intégrée au noyau de PHP, n'a plus été mise à jour depuis 2007. Elle est donc supprimée du noyau de PHP 7.2, mais reste disponible via PEAR.
  • Make Libsodium a Core Extension : intégration de la librairie de cryptage Sodium dans le noyau de PHP 7.2.

PHP 7.2 devrait être installable facilement sous Linux via le package ondrej, qui nous fournit depuis quelques années une installation facile de plusieurs versions de PHP en parallèle, toujours à jours.

Changelog PHP 7.2

Symfony 4 est là !

Ca y est, Symfony 4 est disponible !

Parmi les grosses nouveautés :
  • Symfony Flex : installation plus rapide des dépendances. Pour l'instant je suis pas vraiment convaincu, l'installation n'étant déjà pas compliquée, et ne se fait pas tous les jours. Pas sur que ça change réellement quelque chose, une fois la hype de la 1ère installation passée.
  • Auto-registered & autowired services : définition automatique des services, en fonction des paramètres attendus dans le constructeur par exemple. Pratique pour ne plus avoir de fichiers de config complexes, le revers de la médaille c'est qu'on a encore une couche de magie supplémentaire. A voir !
  • Installation plus light : Symfony 4 est 70% plus léger que Symfony 3.4 à l'installation. Beaucoup de dépendances pas forcément utiles à tous les projets ne sont pas installées par défaut. Ca, c'est top !
  • Performances : benchmark #1, benchmark #2, benchmark #3. C'est en bonne voie, mais concrètement, les benchmarks n'indiquent pas de réelles améliorations en prod. A voir quand phpbenchmarks.com aura fait ses benchmarks pour comparer.
  • Directory structure : pas mal de modifications dans l'architecture des répertoires du projet (pas de Symfony ou des bundles). Ca ressemble plus à du conventionnel, orientation bundle-less pour notre projet (depuis le temps qu'ils veulent se débarasser de ça !), etc. A voir ce que ça va nous apporter, en tout cas l'idée d'être plus conventionnel, oui !
Annonce de la sortie de Symfony 4
Changelog Symfony 4
Symfony 3.4 (Symfony 4 avec les deprecated)

mercredi 4 octobre 2017

DoctrineIssue #6751 : EntityRepository et query hints par défaut

Si vous définissez des query hints par défaut, pour toutes vos requêtes, sachez que les méthodes de EntityRepository ne les gèrent pas forcément !

En l'occurence, findAll(), findBy() et findOneBy() n'ajoutent pas ces hints par défaut.

Issue #6751

mercredi 20 septembre 2017

Configurations par défaut des bundles

Quand on configure un bundle, par exemple dans app/config/config.yml, ces 2 configurations devraient avoir le même comportement :
framework: # pas de clef form
framework: form: ~
Mais le comportement est différent.

Pour le premier cas, sans la clef form, le composant Form de Symfony n'est pas chargé, parce que la configuration par défaut de framework.form.enabled est à false.
Dans le 2ème cas, le composant Form est chargé, alors qu'on s'attend à ce que ~ soit équivalent à "prend les valeurs par défaut".

Peut-être que c'est une sorte de raccourci, pour activer le composant Form, sans avoir à définir framework.form.enabled à true.
Si c'est le cas, la documentation ne le mentionne pas, et il faudrait qu'on ait une liste des configurations qui diffèrent entre ne rien mettre, et mettre ~.
Issue #24269

mercredi 5 juillet 2017

Issue #23406 : Doctrine bigint casté en string depuis la 3.2.10

Une PR, validée pour Symfony 3.2.10 mais pas les autres versions (2.7, 2.8 et 3.3, à voir si la PR n'était pas déjà présente sur ces branches) casse tous les typages PHP pour le type Doctrine bigint.

Ils ont décidé de caster les bigint Doctrine en string PHP, au lieu de int PHP (comme avant).
Tous les typages PHP7 sont cassés, et toutes les comparaisons typés avec === également.

En espérant qu'ils corrigent ça vite, le type bigint est sûrement utilisé par beaucoup de gens pour les identifiants de table !

Issue #23406

mardi 27 juin 2017

Doctrine issue #6509 : PersistentCollection et orphanRemoval

Remontée de bug pour Doctrine 2.5.x (et probalement les versions précédentes) : si on appelle PersistentCollection::clear() ou PersistentCollection::removeElement(), et que la liaison a orphanRemoval, alors les éléments supprimés seront enregistrés dans l'UnitOfWork comme étant à supprimer en base de données.

Jusque là, tout va bien. Mais si on veut de nouveau ajouter un élément dans la PersistentCollection, et que cet élément a été indiqué comme étant à supprimer avant, alors PersistentCollection n'annule pas la demande de suppression.

Résultat : l'élément supprimé, puis re-ajouté, est supprimé en base, alors qu'on voulait le conserver.

Issue #6509

mercredi 21 juin 2017

Symfony Issue #23248 : DataCollectorTranslator::getFallbackLocales() retourne un tableau vide

En environnement de développement, si on appelle $container->get('translator')->getFallbackLocales(), on a un tableau vide.

La surcharge DataCollectorTranslator, qu'on n'a qu'en environnement de développement (pour le profiler), gère correctement les fallbacks si le service translator est une instance de Translator.
Pour les autres implémentations, fournies par Symfony (LogginTranslator par exemple), DataCollectorTranslator retourne un tableau vide.

DataCollectorTranslator.php#100
Issue #23248

vendredi 2 juin 2017

[symfony/validator] Issue #23032 Valider un tableau valide les valeurs récursivement

Si on valide un tableau, qui contient un objet parmi ses valeurs, alors cet objet sera validé, avec le groupe de validation utilisé pour le tableau.

C'est différent pour les objets, qui ont besoin du validateur Valid pour avoir le même comportement.

#23032