Navigation

Related Articles

Back to Latest Articles

Simplifiez vos formulaires Symfony


Gilles
Simplifiez vos formulaires Symfony

Les formulaires Symfony permettent de soumettre des données, de les valider, et pourquoi pas de transformer la valeur en une classe.

On peut aussi y associer une classe et laisser le formulaire mettre à jour les propriétés de celle-ci sans notre intervention.

Est-ce une bonne chose ? Pourquoi ne pas laisser nos formulaires faire le moins de choses possible ? Laissez moi vous expliquer en commençant par revoir quelques bases de séparation des préoccupations (separation of concerns).

En voiture 

Vous avez une entité Voiture -> Marque, Moteur, Couleur, nombre de portes, climatisation, transmission, année.

On vous demande de créer en base de données une Voiture avec tous ces champs obligatoires.

Votre premier réflexe est d’utiliser peut-être symfony maker, make:entity.

Le code qui va générer les getters et setters pour chaque propriétés.

Avant de parler du formulaire, qu’est-ce qui devrait vous choquer déjà ? 

Moi c’est le fait qu’on nous impose du code, alors qu’on a rien demandé.

Comme si écrire car->setMotor avait du sens ? ou car->setNbDoors?

Reprenons : au tout début on nous oblige à créer une voiture avec tous les champs obligatoires, mais on ne nous a pas demandé de pouvoir changer le moteur ou le nombre de portes.

Posons la question à l’intéressé:

Développeur : Euh…on peut changer le moteur d’une voiture et son nombre de portes n’importe quand ?
Intéressé : Pardon ? je ne comprends pas
Développeur : Voilà dans mon code j’ai rajouté des setters pour pouvoir changer le moteur n’importe quand, ça a du sens pour vous ?
Intéressé : Je ne pense pas. D’ailleurs que sont ces fameux sept heures ?
Développeur : C’est pour changer la valeur d’une propriété d’une classe. Par exemple dans la classe Voiture, on pourrait changer n’importe quand le moteur par un autre.
Intéressé : On ne change pas le moteur d’une voiture n’importe quand, c’est étrange de faire ça non ?
Développeur : Oui vous avez raison…C’est parce que j’ai utilisé un outil qui m’a généré du code automatiquement et…. “Nom de Zeus !”

Du code généré automatiquement vous a conduit à poser une série de questions pertinentes, néanmoins était-il pertinent de laisser ce code généré automatiquement ?

Sûrement pas, c’est pour cela que quand j’utilise le maker de symfony pour ajouter une propriété dans mon entité, parce que c’est pratique, je vais supprimer les setters directement, même ceux inclus dans les méthodes Add*.

Revenons à notre code pour créer une voiture.

Est-ce que ceci vous semble intuitif :

Ou bien le code suivant :

Ce qui nous donnerait à l’utilisation :

La seconde méthode nous permets d’avoir un meilleur contrôle sur les paramètres qui vont être assignés à l’entité. On pourrait aller plus loin et mettre des Assertions pour s’assurer qu’on a bien le type attendu. 

Mais vous allez vous dire, pourquoi j’ai besoin de faire ça, c’est mon code, je le connais, je connais l’ordre des paramètres !? Il y a une différence entre connaître les paramètres et ne pas avoir besoin de les connaître.

Ceux qui utilisent Doctrine et les Embeddable pourraient être tentés d’en créer, mais finalement ce qui importe c’est de sécuriser votre entité, non de rajouter des classes.

Et le rapport avec les formulaires ?

Symfony a un composant génial, le FormComponent. Hélas il a souvent été associé à des entités doctrine pour plus de simplicité et de rapidité.

Et pour qu’entité et form fonctionnent, il faut des getters/setters (pas que) sur chaque propriété de votre entité. De plus, quel est le but d’un formulaire ? Afficher une balise form dans une page html et récupérer les valeurs. Le lien entité/formulaire qui s’est installé dans les pratiques des développeurs est pour moi une très mauvaise habitude.

De un, on nous force à ajouter du code dans notre entité. De deux on peut mettre à jour une entité directement via un formulaire, sans intermédiaire. Et pour finir on va sûrement polluer notre entité avec des groupes de Validation (NotBlank, NotNull, Email etc).

Mais le pire c’est qu’on va polluer le code de notre projet pour l’adapter à un framework.

Symfony est un framework, une boîte à outils. Le FormComponent est capable de faire le lien entre les propriétés d’une classe et les champs du formulaire via le PropertyAccessComponent, c’est très pratique. La documentation de Symfony vous montre comment faire, mais ils n’expliquent pas, et ça n’est pas leur rôle, si c’est une bonne pratique de mettre une entité directement sur un formulaire pour pouvoir enregistrer en base de données dans la foulée.

Votre projet devrait respecter le principe open/closed au niveau du framework utilisé !

Je m’explique. Vous avez sûrement entendu parler d’architecture hexagonale, de clean architecture, de code SOLID ? 

https://blog.octo.com/architecture-hexagonale-trois-principes-et-un-exemple-dimplementation/

Le principe repose sur le fait d’isoler votre code de l’extérieur, si vous avez besoin de faire appel à des bases de données, des requêtes http, des traductions, des validations, des contrôleurs etc…Des outils nécessaires mais externes à votre code, on va utiliser des interfaces pour communiquer avec l’extérieur. 

Ainsi avec le composant Messenger de symfony, au lieu d’injecter directement leur interface MessageBusInterface, préférez plutôt votre interface. Vous pouvez alors choisir le Messenger de symfony et votre code devient moins couplé à une librairie externe, et vos tests vont vous dire merci !

Bon et bien pour les formulaires c’est pareil.

Faites des formulaires qui ne soient jamais reliés à une classe ou une entité.

Le seul intérêt d’un formulaire, c’est de récupérer les valeurs saisies et d’en faire ce qu’on veut ensuite. Pourquoi aurait-on besoin d’y associer une classe ?

En adoptant cette méthodologie, vous évitez de mettre à jour directement une entité ou de créer une classe “model” pour votre formulaire qui a peu d’intérêt.

Parenthèse sur les contraintes de validation

Si on a plus de classe model, ni d’entité, les contraintes de validation doivent bien aller quelque part. Et bien elles sont directement ajoutées dans votre formulaire. J’entends déjà des protestations au fond : Pourquoi découpler son formulaire si c’est pour lui ajouter des validations ?

Reprenons, un formulaire c’est fait pour soumettre des valeurs. Mais il faut bien s’assurer que les valeurs soumises correspondent à la valeur attendue. On ne parle pas de logique métier très poussée comme une validation sur un email unique en base de données. Non, ici on veut juste s’assurer que le champ email est bien un email, que le champ année est bien sélectionné, qu’une valeur est inférieure ou supérieure à une limite donnée etc.

Le formulaire et les validations de base vont de paire. Si vous avez besoin de vous assurer qu’un email n’est pas déjà utilisé en base de données, ça n’a rien à faire dans un formulaire, c’est une information qui provient de votre base de données, une api, un fichier excel….et donc une validation qui doit intervenir après avoir appelé $form->isValid().

Fin de parenthèse

Les données, valides et récupérées, on va les passer à un certain type de classe, un handler

Un handler est une classe qui va effectuer une et une seule chose. On peut être tenté de faire un super manager comme en voit dans les anciennes écritures, avec des milliers de lignes de codes que personne ne peut déchiffrer. En créant une classe par action, vous allez abaisser la complexité de votre code, il sera plus simple à maintenir et à lire. 

Symfony propose le composant Messenger, très pratique surtout pour faire de l’asynchrone. Il propose aussi un middleware avec doctrine pour effectuer un flush à la fin d’un handler, ça vous évite de devoir injecter entityManager dans votre handler.

Il y aussi SimpleBus, que personnellement je préfère utiliser et qui propose un MessageBus et un EventBus. 

En fait j’utilise souvent celui de Symfony pour les messages asynchrones et le reste avec SimpleBus. C’est une question de goût, à tester.

Petite précision, au moment de l’écriture de cet article, j’ai eu l’information comme quoi SimpleBus ne serait plus maintenu sur les nouvelles versions de Symfony et les mainteneurs encouragent à utiliser le Messenger de Symfony. Donc n’utilisez pas le SimpleBus.

Conclusion

On l’a vu, les formulaires de Symfony peuvent vite devenir complexes alors que leur but principal est de récupérer les valeurs soumises par un utilisateur.

Les valeurs qu’on récupère peuvent alors être manipulées, analysées, sauvegardées par une classe dédiée.

On privilégie ainsi une bonne séparation des préoccupations, chaque étape est ainsi isolée des autres.

J’espère que cet article vous fera réfléchir à certains points et n’hésitez pas à donner votre avis !

Show Comments (6)

Comments

  • Thomas

    Bonjour,
    Merci pour votre article, toujours intéressant de vous lire !

    On retrouve régulierement ce type de conseils visant à délaisser le mapping form/entité au profit de l’usage de DTO ou d’un simple array de data comme dans votre article. Les exemples donnés sont toujours sur des cas extrêmement simples mais l’intérêt d’un formulaire réside aussi dans sa gestion des collections (ex: entité many-to-many), des imbrications de forms, etc. Ces usages avancées sont souvent requis par des besoins d’interfaces utilisateur complexes et c’est difficile (et ça serait bête) de s’en passer.
    Peut-être pour les besoins de l’exemple, vous initialisez votre CreateCar() directement dans le controller. Pour des besoins plus complexes et réutilisables, je pense qu’il est requis de passer par des services intermédiaires pour renseigner nos models à partir des data du form, surtout dans le cas où nous avons des datas complexes faisant intervenir plusieurs entités (collections, imbrications, etc.), et là nous nous retrouvons à passer des arrays de valeurs de services en services, ce qui me ferait bien regretter l’usage d’un model.
    Actuellement, dès qu’un cas un peu complexe se présente, je passe par un model et je définis les contraintes sur ce model, mais j’ai régulièrement l’impression de reproduire une grosse portion des propriétés de mes entités sur ces models et la gestion des collections d’entités (création, mise à jour, suppression) dans les services est complexe et me fait souvent regretter de ne pas laisser les forms Symfony faire le boulot directement au niveau des entités.
    Pour les cas simples, j’ai tendances à conserver les interactions directes form/entité et bien que j’apprécie beaucoup votre approche, je me demande parfois si « ça vaut le coup » de se passer de cette facilité offerte par le composent de form.

    J’ai également un peu de mal à l’idée de valider mes données sur plusieurs niveaux (je parle de votre exemple du doublon d’email en base qui sera validé hors form) et pour lequel il faudra trouver un moyen élégant de remonter l’erreur à l’utilisateur.

    Je n’apporte pas d’avis spécifique ni une quelconque solution dans mon post, il s’agit juste de mes réflexions et de mon approche actuelle, même si je trouve cela plutôt imparfait.

    • Article Author
  • Gilles

    Bonjour Thomas,

    Merci pour cette réponse constructive !
    Oui l’exemple reste simple, je ferai un exemple plus complexe !

    Je ne remets pas en cause l’utilisation des models/entitées associés aux formulaires, je remets en cause leur utilisation régulière, automatique.

    C’est très pratique comme vous le dites, mais pratique ne signifie pas toujours le meilleur choix.

    J’ai longtemps suivi la méthode « enseignée » par symfony pendant des années, et force d’essayer et de regarder ce qui se fait ailleurs, de découpler plus simplement j’en suis arrivé à ce constat (qui n’est peut-être pas la meilleure pratique !)

    Mais aujourd’hui je n’utilise plus de model sur mes formulaires, mêmes ceux complexes…je me focalise en premier sur le traitement des données dans mes handlers, et une fois satisfait je commence mes formulaires. Souvent c’est le sens inverse qui est utilisé !

    Si vous voulez en discutez davantage je suis disponible sur le slack de Symfony @GillesG 😉

    • Article Author
  • Thomas

    J’avais bien compris que vous remettiez uniquement en cause l’usage systématique du mapping form/doctrine, et je fais de même lorsque je juge que le cas est complexe et peut être amené à évoluer. Je comprends également les approches plus « extrêmistes » qui préconisent de ne jamais mapper son form directement sur une entité (les entités sont des models précieux, le form va potentiellement les renseigner avec des données invalides et nous ne sommes alors qu’à un flush d’un gros soucis).
    Mais effectivement, il faut savoir rester pragmatique et choisir la voie complexe uniquement quand c’est requis.
    Ce qui me chiffonne avec une approche sans model (basée sur un array) pour gérer un cas complexe, c’est qu’on met en place une structure découplée et censée être évolutive (multiples services chargés des validations/persist des différents composants de nos data, etc.) en se basant sur un format de support/transfert de données dépourvu de typage et autres fonctionalités d’accessibilité ou de validation bien pratiques. Bref, j’ai l’impression qu’on effectue un dev lourd en se privant des objets qui sont parfaitement pensés pour ces besoins.

    Je me cherche toujours et tout avis complémentaire est le bienvenue 🙂

    Si d’autres avis constructurs

    • Article Author
  • Fabien

    Pas convaincu.

    Avoir des données potentiellement non valides après un $form->isValid(), ça ne fait pas sens.

    Le système de validation de Symfony est très poussé et permet de créer ses propres contraintes de validations, et de choisir selon chaque contexte quelles contraintes vérifier ou non grâce aux groupes de validation.

    « Faites des formulaires qui ne soient jamais reliés à une classe ou une entité. » Pourquoi ? Il ne s’agit pas de couplage, mais d’une spécialisation : c’est un formulaire en charge des données d’une certaine entité tout comme une contrainte de validation est en charge d’un certain type de données.

    Quels sont les bénéfices à ne pas lier un formulaire à une entité ?

    • Article Author
  • Gilles

    Bonjour Fabien,

    Le principe que j’essaie de montrer dans l’article justement c’est de ne pas lier entité et formulaire.

    « Le formulaire est en charge des données d’une certaine entité »
    Pour moi non, l’entité représente le mapping de la table en BDD rien de plus.
    Donc si on veut la mettre à jour, passer par un formulaire nous contraint à ajouter du code pour que le formulaire puisse fonctionner, idem avec les validations.

    Le code de l’entité devrait rester simple et cohérent, et non s’adapter à ce qui vient de l’extérieur.

    Admettons que j’ai un formulaire qui soumets des données et qui sont validées par symfony. Ces données sont ensuite transmises à un handler, qui connait la logique métier du projet et qui va faire un traitement dessus, et seulement à cette étape on va mettre à jour l’entité.

    Le bénéfice c’est qu’on peut facilement tester et identifier qui fait quoi sur une entité. Plus simplement qu’une armée de groupe de validation et de formulaire.

    Pour information c’est en regardant et lisant la pratique du DDD et du clean architecture (et quelques années de tests et d’echec) que j’en suis arrivé à cette approche qui pour l’instant me satisfait.

    Si tu veux en discuter plus je suis dispo sur le slack symfony @GillesG #french 😉

    • Article Author
  • Fabien

    Bonjour Gilles,

    L’entité n’est pas le mapping. L’entité est un objet métier qui est mappé (ou non) à la base de données grâce au mapping qui peut prendre la forme d’annotations.

    Le code de l’entité reste simple et cohérent, puisque que les contraintes de validations ne sont pas du code, mais de simples annotations (ni plus ni moins que des commentaires). Et ces contraintes peuvent même être déclarées hors des classes des entités en utilisant le format YML par exemple.

    Il n’est pas difficile de tester les validateurs et les formulaires avec l’approche « formulaire mappé sur une entité ». Du coup, je ne vois pas vraiment le gain de votre approche, si ce n’est une question d’affinités.

    • Article Author

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Recevez nos articles