Logo SmartBooster Playbook

Conventions de code

Un outil par responsabilité, la qualimétrie qui les enchaîne, et comment un projet existant rejoint le niveau.

Une convention de code est une décision prise une fois pour que personne n’ait à la reprendre : où va l’accolade, quel guillemet, quel ordre pour les attributs d’un composant, quel niveau d’exigence sur les types. Prise par une personne et répétée par un outil, elle sort de la revue de code, de la discussion, et de la tête de chacun.

Cette page décrit la chaîne d’outils que tout notre code traverse avant d’être livré, côté PHP et côté front, et le principe qui la tient : un outil par responsabilité. Elle dit aussi comment un projet qui n’avait rien de tout cela rejoint le niveau, parce que c’est le cas de la plupart des projets que nous reprenons.

Pourquoi nous y tenons autant est raconté ailleurs : dans l’article mieux travailler ensemble, qui montre que les conventions de code et les conventions d’équipe sont le même mécanisme.

La qualimétrie : un mot, une commande

La qualimétrie, c’est la mesure de la qualité du code par des outils, sans l’exécuter. Le mot est ancien et peu connu, mais il nomme exactement ce que nous faisons : une série de contrôles automatiques, chacun avec sa question, qui rendent un verdict vert ou rouge. Chez nous, c’est aussi un nom de fichier, un job de CI et une commande.

Le standard-bundle, embarqué dans chaque projet Symfony que nous livrons, apporte un fichier make/qualimetry.mk. Sa cible qualimetry, raccourcie en qa, enchaîne tous les contrôles PHP :

qualimetry: phpstan checkstyle psalm-ci lint-php lint-twig lint-yaml lint-container composer-validate
qa: qualimetry

make qa est la commande que l’équipe lance avant de pousser dès qu’elle a touché du code, et celle que la CI exécute sur chaque merge request. Côté front, yarn validation joue le même rôle. Une merge request dont la qualimétrie est rouge ne se relit pas : le relecteur attend qu’elle soit verte pour regarder ce qui compte.

Un outil, une responsabilité

Chaque outil de la chaîne a une question, et une seule. Aucun n’empiète sur un autre : un échec nomme son responsable, deux outils ne se contredisent jamais, et chacun se configure et se met à jour seul.

ResponsabilitéCôté PHPCôté front
La forme : indentation, guillemets, ordrePHP CS FixerPrettier
Le fond : erreurs, conventions d’écriturePHPStanESLint
Les typesPHPStan, niveau maximalvue-tsc, sur les projets TypeScript
La sécurité : flux de données dangereuxPsalm, en analyse de teinte seulementPas d’équivalent : le front n’exécute ni SQL ni commande
La syntaxe des fichiers annexesphp -l, lint:twig, lint:yaml, lint:containerPris en charge par ESLint et Prettier
Les dépendancescomposer validate, composer auditHors chaîne front à ce jour

Cette séparation n’est pas une élégance : PHPStan et Psalm savent tous deux vérifier des types, ESLint sait formater. Nous bridons chacun à sa case. Le jour où deux outils rapportent la même chose, l’équipe ne sait plus lequel croire, et corrige l’un pour faire échouer l’autre.

La chaîne PHP

Tout arrive par la recette Symfony Flex du standard-bundle : configuration, baselines vides, cibles Make. Un projet neuf a la chaîne complète dès son premier commit.

La forme : PHP CS Fixer. Le ruleset @Symfony, celui que le framework recommande, avec deux écarts seulement, chacun justifié dans le fichier :

->setRules([
    '@Symfony' => true,
    'class_definition' => [
        'multi_line_extends_each_single_line' => true,
    ],
    'concat_space' => ['spacing' => 'one'], # the @PER like PSR12 preserve space
])

Pourquoi PHP CS Fixer plutôt que PHP_CodeSniffer, que nous avons utilisé des années, est expliqué sur sa page.

Le fond et les types : PHPStan au niveau 10. Le niveau le plus strict, sur tout projet, neuf ou repris. Les extensions Symfony, Doctrine et PHPUnit lui font comprendre le conteneur, les entités et les assertions. L’extension disallowed-calls y ajoute une liste d’appels interdits avec, pour chacun, le motif et le remplacement :

-
    function: 'exec()'
    message: 'for security reasons. Use Symfony Process component instead.'
-
    function: 'md5()'
    message: 'md5 is cryptographically broken. Use hash() with a strong algorithm, or password_hash() for passwords.'

La sécurité : Psalm en analyse de teinte. Psalm suit une entrée utilisateur jusqu’à la requête SQL ou la sortie HTML qu’elle atteint. Il est bridé pour ne faire que cela, et ne pas concurrencer PHPStan sur les types. Un stub maison déclare les méthodes Doctrine comme points d’arrivée : sans lui, l’analyse est verte et ne détecte rien.

Le reste. Les lints natifs de PHP et de Symfony sur les fichiers Twig, YAML et la définition des services, puis composer validate et composer audit, qui bloque sur toute CVE active d’une dépendance.

La chaîne front

Le bundle documente le standard front dans docs/front_code_validation.md, mais ne copie aucune configuration : la chaîne s’installe par projet, en couches. Un socle commun, puis ce que le contexte ajoute (Vue, Symfony, Inertia, Tailwind, TypeScript).

Les deux outils sont ceux de l’écosystème Vue. Prettier et ESLint sont ce que le dépôt Vue core utilise, ce que la documentation Vue recommande, et ce que create-vue, l’outil officiel de création de projet, installe. Suivre create-vue garantit de rester aligné sur les pratiques de l’écosystème sans arbitrage de notre part. L’autre candidat, @antfu/eslint-config, qui fusionne règles et formatage en un seul outil, a été écarté : configuration personnelle, opinions de style imposées, et coupure des plugins dédiés comme le tri des classes Tailwind.

Les scripts. Un verbe nu corrige, :check ne fait que rapporter, et validation enchaîne les deux contrôles en affichant les deux rapports :

"scripts": {
  "lint": "eslint . --fix --cache",
  "lint:check": "eslint .",
  "format": "prettier --write .",
  "format:check": "prettier --check .",
  "validation": "yarn lint:check; s=$?; yarn format:check && exit $s"
}

La forme : Prettier, quatre options. semi et printWidth viennent du template create-vue. singleQuote: false est notre seul écart : Prettier choisit le guillemet qui demande le moins d’échappements, et des chaînes françaises pleines d’apostrophes finiraient mélangées. singleAttributePerLine applique le style guide Vue. Sur un projet Tailwind, le plugin officiel trie les classes dans un ordre canonique : plus aucun débat sur l’endroit où va une classe.

Le fond : ESLint et le plugin Vue. Le preset strongly-recommended-error plutôt que essential, le défaut de create-vue, parce que ce dernier n’attrape que les erreurs. Le niveau retenu ajoute les règles de cohérence qui évitent que l’écriture des composants dérive d’un développeur à l’autre : nommage des props, des événements, valeur par défaut de chaque prop optionnelle. Le niveau recommended, au-dessus, a été mesuré et écarté : 752 violations sur deux de nos bases de code, dont 84 % sur l’ordre des attributs dans une balise, une réécriture de tous les templates sans gain fonctionnel.

La frontière. eslint-config-prettier, importé en dernier dans la configuration ESLint, désactive chaque règle ESLint qui formate. Une commande vérifie qu’aucune n’a été oubliée :

npx eslint-config-prettier <fichier.js> <fichier.vue>
# No rules that are unnecessary or conflict with Prettier were found.

Le détail de chaque couche, avec les extraits de configuration, est sur la page ESLint.

Sur un projet existant

Un projet qui n’a jamais eu ces outils en remonte des milliers. Le big bang, corriger tout avant d’activer quoi que ce soit, ne se produit jamais : il coûte des semaines sans livrer, et le projet reste sans filet pendant ce temps. Chaque outil de la chaîne a donc sa façon de n’échouer que sur le code nouveau, dès le premier jour :

  • PHPStan : niveau 10 tout de suite, et une baseline qui fige les erreurs existantes. Tout développement nouveau naît au niveau maximal, l’ancien code attend son tour.
  • Psalm : sa propre baseline de teinte, séparée. La CI ne bloque que sur un nouveau flux dangereux.
  • PHP CS Fixer : chaque règle en défaut est passée à false dans la configuration, puis réactivée une par une, un commit par règle, pour un historique lisible.
  • ESLint : un fichier de suppressions qui compte les violations existantes par fichier et par règle. Une violation corrigée rend son entrée obsolète et le contrôle le signale.
  • Prettier : pas de baseline possible, deux voies. Le pragma @format en tête des fichiers déjà formatés, pour y aller fichier par fichier. Ou un commit unique qui formate tout, dont le hash est inscrit dans .git-blame-ignore-revs pour que git blame continue de montrer l’auteur réel de chaque ligne.

Ces baselines ne sont pas une dette qu’on oublie. Leur résorption entre dans le flux non prioritaire de la maintenance technique : le travail que nous menons en autonomie, financé par un budget annuel dimensionné avec le client, entre deux demandes. Une session de correction prend un lot d’entrées de la baseline, les traite, régénère le fichier, et le diff de la merge request montre le chemin parcouru. Le projet rejoint ainsi progressivement le niveau de qualité que nous appliquons à un projet neuf, sans jamais bloquer une livraison pour cela.

Une entrée de baseline est un contrat : elle dit « cette erreur existait avant nous ». Quand une correction en fait disparaître une, le contrôle échoue sur l’entrée devenue inutile. C’est voulu : le fichier ne peut que diminuer, jamais rester faux en silence.

Autour du code : commits et décisions

Les messages de commit tiennent en trois règles. Tout commit est lié à un ticket de gestion de projet, son identifiant en préfixe. Le message dit l’objectif de la modification, pour qu’un git blame ou une recherche de bug retrouve le pourquoi sans demander à personne. Et il ne paraphrase pas le code : git diff est là pour ça.

Les décisions d’outillage s’écrivent. Quand nous retirons ou remplaçons un outil de la chaîne, la raison est consignée dans un ADR du standard-bundle, daté, public. Celui qui se demandera dans deux ans pourquoi PHP_CodeSniffer n’est plus là trouvera la réponse, et ce qui a remplacé chacune de ses règles.