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é PHP | Côté front |
|---|---|---|
| La forme : indentation, guillemets, ordre | PHP CS Fixer | Prettier |
| Le fond : erreurs, conventions d’écriture | PHPStan | ESLint |
| Les types | PHPStan, niveau maximal | vue-tsc, sur les projets TypeScript |
| La sécurité : flux de données dangereux | Psalm, en analyse de teinte seulement | Pas d’équivalent : le front n’exécute ni SQL ni commande |
| La syntaxe des fichiers annexes | php -l, lint:twig, lint:yaml, lint:container | Pris en charge par ESLint et Prettier |
| Les dépendances | composer validate, composer audit | Hors 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 à
falsedans 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
@formaten 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-revspour quegit blamecontinue 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.