Le problème
Prenons une boutique en ligne organisée en monorepo : les
services Symfony order, stock,
shipping, billing et le package
partagé app-contracts. L'ensemble est orchestré
par moon. Sans outillage de qualité commun, les mêmes
défauts reviennent d'une pull request à l'autre.
- Les revues de code discutent d'indentation et d'ordre des imports, au lieu du comportement.
-
Un processor du service
orderappelle$order->getCustomer()->getEmail(), alors quegetCustomer()peut renvoyernull. L'erreur apparaît en production, sur une commande sans client rattaché. -
Une entité
Reservationdu servicestockimporte une classe de ressource API. Le schéma de base et le contrat d'API se retrouvent liés. - Un argument de service mal typé ne casse qu'à l'exécution, quand le service est instancié.
- Une faille publiée sur une dépendance n'est découverte qu'au hasard d'une mise à jour.
La réponse habituelle s'appelle SonarQube. Son édition gratuite auto-hébergée, Community Build, n'analyse que la branche principale : le constat arrive après la fusion. Les éditions auto-hébergées qui analysent les pull requests sont payantes.
Sans ce budget, la question devient : quels outils gratuits couvrent ces défauts, et à quel moment les lancer ?
Les options
SonarQube Community Build seul
Community Build est gratuit, auto-hébergé et publié chaque mois. Il analyse le PHP : sa documentation annonce une prise en charge complète de PHP 5.0 à 8.4.
- Ce qu'il règle : un tableau de bord unique, un historique de la dette, des règles maintenues par l'éditeur.
- Ce qu'il coûte : un serveur à exploiter. Surtout, il n'analyse que la branche principale : ni les autres branches, ni les pull requests. La détection des vulnérabilités d'injection (l'analyse de flux, ou taint analysis) n'y figure pas.
Un plugin communautaire, non supporté par SonarSource, ajoute les branches et les pull requests. Sa version suit celle du serveur : au 1er octobre 2026, il vise la 26.5, quand Community Build en est à 26.9.
Une édition payante de SonarQube
- Ce qu'elle règle : l'analyse des branches et des pull requests, et le statut de la quality gate sur chaque pull request.
- Ce qu'elle coûte : pour SonarQube Server, une licence par instance et par an, calculée sur le nombre de lignes de code.
L'offre gratuite de SonarQube Cloud analyse aussi les pull requests qui visent la branche principale, jusqu'à 50 000 lignes de code privé. Le code est alors analysé hors de vos murs.
Une pile d'outils open source, lancée par le monorepo
- Ce qu'elle règle : chaque outil cible une famille de défauts. Il tourne en local comme en CI, et sa configuration vit dans le dépôt. Son code de sortie suffit à bloquer une pull request.
- Ce qu'elle coûte : plusieurs outils à configurer et à mettre à jour, et pas de tableau de bord consolidé.
Un outil tout-en-un
Deux outils visent ce créneau. PHP Insights agrège PHP-CS-Fixer, PHP_CodeSniffer et Slevomat Coding Standard dans un rapport noté. Mago, écrit en Rust, réunit formateur, linter, analyseur statique et règles d'architecture dans un seul binaire.
- Ce qu'il règle : une installation, une configuration, une seule sortie.
- Ce qu'il coûte : PHP Insights recouvre les outils qu'il agrège, et sa version 2.15 demande PHP 8.4. Mago est jeune : version 1.0 en décembre 2025, version 1.50 le 21 septembre 2026.
Notre recommandation
PHPStan au niveau 10, PHP-CS-Fixer, Rector, Deptrac,
composer audit et les linters Symfony, lancés
par moon en local comme en CI.
Chaque outil attrape une famille de défauts que les autres ne voient pas. Une baseline PHPStan absorbe la dette existante, pour viser le niveau maximal dès le premier jour. La configuration est versionnée avec le code, et un développeur obtient en local le verdict de la CI.
| Outil | Ce qu'il attrape |
|---|---|
| PHP-CS-Fixer | style, imports, syntaxe à moderniser |
| Linters Symfony et Doctrine | YAML invalide, service mal câblé, mapping incohérent |
| PHPStan et ses extensions |
types, appels sur null, DQL invalide
|
| Deptrac | dépendance interdite entre couches |
Rector en --dry-run |
code à migrer vers la version cible de PHP ou Symfony |
composer audit |
dépendance vulnérable ou abandonnée |
| PhpMetrics | complexité, couplage, maintenabilité par classe |
Selon le contexte, nous ajoutons :
-
Psalm, pour sa seule analyse de flux
(
--taint-analysis), avecpsalm/plugin-symfonyqui l'étend à Symfony. Elle suit une entrée utilisateur jusqu'à une requête SQL ou une sortie HTML. C'est ce qui manque à Community Build. -
Twig CS Fixer et
lint:twig, pour un service qui rend des templates, comme les e-mails deshipping. -
Infection, sur le code le plus sensible,
comme le calcul des montants de
billing. Les tests de mutation révèlent les tests qui passent sans rien vérifier. En pull request,--git-diff-lineslimite les mutations aux lignes modifiées. - PHP_CodeSniffer, si une norme de codage dont vous avez besoin n'existe que pour lui. PHPCSStandards le maintient depuis l'abandon du dépôt d'origine, sous le même nom de paquet.
Nous laissons de côté :
- PHPMD : sa dernière version stable, la 2.15.0, date du 11 décembre 2023. La branche 3.x est active, mais aucune version 3 n'est publiée au 1er octobre 2026.
- GrumPHP : il installe ses propres hooks git depuis un projet Composer. Dans un monorepo, moon gère déjà les hooks, avec les mêmes tâches que la CI.
- PHP Insights : il recoupe PHP-CS-Fixer, déjà dans la pile, et sa version 2.15 dépend encore de PHP_CodeSniffer 3.
- Mago, pour l'instant : nous l'évaluons sur une branche, sans bloquer, avant de lui confier une pull request.
Les compromis assumés
Plusieurs outils au lieu d'un tableau de bord.
Chaque outil a sa configuration, sa sortie et ses montées de
version. Nous les traitons comme les autres dépendances de
développement : épinglés dans composer.lock,
mis à jour par pull request.
Pas de vue consolidée. Personne ne voit
d'un coup d'œil la dette de tout le monorepo. PhpMetrics et,
le cas échéant, Community Build sur
main comblent une partie de ce manque.
Une baseline à faire décroître. Elle accepte la dette existante pour bloquer la nouvelle. Elle ne décroît que si quelqu'un s'en occupe, et une baseline qui grossit se refuse en revue.
Un niveau 10 exigeant. À ce niveau, PHPStan
refuse toute opération sur une valeur mixed,
même implicite. Le code qui lit des tableaux non typés
(payloads, configuration) demande des annotations ou des
DTO.
Le signal qui doit faire réexaminer ce choix : un audit qui exige des rapports de sécurité consolidés. Ou des rapports séparés que plus personne ne lit. Une édition commerciale de SonarQube redevient alors une option à chiffrer.
Mise en œuvre
Les exemples visent Symfony 7.4 et PHP 8.4 ; ils valent aussi pour Symfony 8. Chaque service installe ses outils en dépendances de développement.
composer require --dev phpstan/phpstan phpstan/extension-installer \
phpstan/phpstan-symfony phpstan/phpstan-doctrine \
friendsofphp/php-cs-fixer rector/rector deptrac/deptrac phpmetrics/phpmetrics
1. PHPStan au niveau 10, avec une baseline
# phpstan.neon (service order)
includes:
- phpstan-baseline.neon
parameters:
level: 10
paths:
- src/
- tests/
symfony:
containerXmlPath: var/cache/dev/App_KernelDevDebugContainer.xml
doctrine:
objectManagerLoader: tests/object-manager.php
Avec phpstan/extension-installer, les
extensions Symfony et Doctrine s'activent seules, règles
comprises. La première lit le conteneur compilé et connaît
le type de chaque service. La seconde charge l'entity
manager (script tests/object-manager.php de sa
documentation) pour vérifier le DQL et l'accord entre
colonnes et propriétés.
Nous écrivons 10 plutôt que max.
L'alias max suit le niveau le plus élevé, et
PHPStan en a ajouté un avec sa version 2.0. Avec un chiffre,
la montée de niveau reste une décision.
La baseline enregistre les erreurs existantes, pour ne bloquer que les nouvelles :
vendor/bin/phpstan analyse --generate-baseline
On lance cette commande avant d'ajouter
phpstan-baseline.neon aux
includes : PHPStan refuse d'inclure un fichier
absent.
Le fichier phpstan-baseline.neon est commité.
Quand une erreur est corrigée, PHPStan signale l'entrée
devenue inutile, car
reportUnmatchedIgnoredErrors est actif par
défaut. On régénère alors la baseline : elle diminue, et une
hausse se voit dans le diff.
2. Le style avec PHP-CS-Fixer
// .php-cs-fixer.dist.php (service order)
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = (new Finder())->in([__DIR__ . '/src', __DIR__ . '/tests']);
return (new Config())
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
])
->setFinder($finder);
En local, vendor/bin/php-cs-fixer fix corrige.
En CI,
vendor/bin/php-cs-fixer check --diff échoue et
affiche la correction attendue. Les règles « risky » peuvent
changer le comportement du code : nous les activons sur un
service couvert par des tests.
3. Rector en mode vérification
// rector.php (service order)
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withPhpSets()
->withComposerBased(doctrine: true, phpunit: true, symfony: true)
->withSymfonyContainerXml(__DIR__ . '/var/cache/dev/App_KernelDevDebugContainer.xml');
withPhpSets() sans argument lit la version de
PHP dans composer.json.
withComposerBased() charge les règles qui
correspondent aux versions installées de Symfony, Doctrine
et PHPUnit. En CI,
vendor/bin/rector process --dry-run échoue s'il
reste du code à migrer.
Pour une montée de version de Symfony, on lance
vendor/bin/rector process sans
--dry-run. Le diff se relit ensuite comme une
pull request ordinaire.
4. Deptrac et les couches du service order
Dans le service order, les ressources API
(src/ApiResource) sont des DTO. Les providers
et processors (src/State) font le lien avec les
entités Doctrine. Deptrac transforme cette règle en
contrôle.
# deptrac.yaml (service order)
deptrac:
paths:
- src/
layers:
- name: Controller
collectors:
- type: directory
value: src/Controller/.*
- name: ApiResource
collectors:
- type: directory
value: src/ApiResource/.*
- name: State
collectors:
- type: directory
value: src/State/.*
- name: Messaging
collectors:
- type: directory
value: src/Messaging/.*
- type: directory
value: src/MessageHandler/.*
- name: Entity
collectors:
- type: directory
value: src/Entity/.*
- type: directory
value: src/Repository/.*
ruleset:
Controller: [Entity, Messaging]
# ApiResource -> Entity : stateOptions(entityClass: Order::class) d'API Platform
ApiResource: [Entity]
State: [ApiResource, Entity, Messaging]
Messaging: [Entity]
Entity: ~
Par défaut, Deptrac interdit toute dépendance entre couches
: le ruleset liste les seules autorisées.
Entités et repositories forment une seule couche, car
#[ORM\Entity(repositoryClass: ...)] lie
l'entité à son repository. La couche Entity ne
dépend de rien : avec les mêmes couches dans
stock, l'entité Reservation du
problème fait échouer la pull request.
Les classes hors couches (Symfony, Doctrine,
app-contracts) ne sont pas contrôlées ici. Pour
une dette existante, --formatter=baseline écrit
un fichier deptrac.baseline.yaml à importer. Le
paquet s'appelle désormais deptrac/deptrac :
qossmic/deptrac est marqué abandonné.
5. Les linters Symfony et Doctrine, et l'audit des dépendances
Symfony, Doctrine et Composer fournissent leurs propres contrôles, sans dépendance supplémentaire :
php bin/console lint:yaml config --parse-tags
php bin/console lint:container
php bin/console doctrine:schema:validate --skip-sync
composer audit --locked
lint:container vérifie que les arguments
injectés correspondent aux types déclarés.
doctrine:schema:validate --skip-sync valide le
mapping sans le comparer à la base de données.
composer audit confronte le
composer.lock aux avis de sécurité. Depuis
Composer 2.7, il échoue aussi par défaut sur les paquets
abandonnés.
6. Les tâches moon, en local et en CI
Chaque service déclare ses tâches dans son
moon.yml :
# moon.yml (service order)
language: php
layer: application
dependsOn:
- app-contracts
tasks:
lint:
script: >-
vendor/bin/php-cs-fixer check --diff
&& php bin/console lint:yaml config --parse-tags
&& php bin/console lint:container
&& php bin/console doctrine:schema:validate --skip-sync
analyse:
script: >-
php bin/console cache:warmup --env=dev
&& vendor/bin/phpstan analyse --no-progress
&& vendor/bin/deptrac analyse --no-progress
&& vendor/bin/rector process --dry-run --no-progress-bar
audit:
command: composer audit --locked
options:
cache: false
runInCI: always
metrics:
command: vendor/bin/phpmetrics --report-html=var/phpmetrics src
La tâche analyse compile d'abord le conteneur
de dev, que lisent PHPStan et Rector. Les avis de sécurité
changent sans que le code change : audit ne
passe donc pas par le cache et tourne à chaque CI.
Le hook de pre-commit ne lance que le lint des projets touchés par les fichiers indexés :
# .moon/workspace.yml
vcs:
defaultBranch: 'main'
hooks:
pre-commit:
- moon run :lint --affected --status=staged
sync: true
Sans defaultBranch, moon prend
master comme base de comparaison quand la CI ne
lui en fournit pas.
En CI, une seule commande lance les tâches des projets affectés par la pull request :
moon ci :lint :analyse :audit
moon ci compare la branche à sa base et ne
garde que les tâches concernées. Il lui faut l'historique
complet du dépôt : un clone superficiel fausse la détection.
Les règles de protection de branche rendent ce job
obligatoire avant la fusion.
7. Le rapport périodique
Un job planifié, sur main, relance l'audit et
le rapport PhpMetrics de tous les projets :
moon run :audit :metrics
Une faille publiée après la dernière pull request apparaît
ainsi sans attendre la suivante. Si Community Build est
installé, le même job lui envoie l'analyse de
main.
PhpMetrics produit un rapport HTML : complexité, couplage et indice de maintenabilité par classe. Sa branche 2.x reste publiée (2.11.0 le 9 août 2026), la 3.0 est en version candidate depuis 2023.
Checklist
- PHPStan au niveau 10, avec les extensions Symfony et Doctrine.
- Une baseline commitée, qui ne grossit pas sans revue.
- PHP-CS-Fixer en correction locale et en vérification en CI.
-
Rector en
--dry-runen CI, et pour chaque montée de version. - Des couches Deptrac, avec des entités qui ne dépendent de rien.
-
lint:yaml,lint:containeretdoctrine:schema:validate --skip-syncà chaque pull request. -
composer audit --locked, hors cache, à chaque CI. - Les mêmes tâches moon en pre-commit et en CI, obligatoires avant la fusion.
-
Un rapport périodique sur
main: PhpMetrics, et Community Build s'il est en place.
Sources
Versions et dates relevées sur Packagist et GitHub le 1er octobre 2026. Dernières versions stables : PHPStan 2.2.16, PHP-CS-Fixer 3.95.27, Rector 2.6.7, Deptrac 4.7.2, Psalm 6.19.1 (7.0 en bêta), Mago 1.50.0.
- SonarSource, Feature comparison table : branches, pull requests et injections selon l'édition.
- SonarSource, PHP et Release cycle model pour Community Build.
- SonarSource, Plans and pricing et SonarQube Cloud subscription plans.
- sonarqube-community-branch-plugin : support et compatibilité.
- PHPStan, Rule Levels et The Baseline.
- phpstan-symfony, phpstan-doctrine et extension-installer.
- Psalm, Security Analysis, et psalm/plugin-symfony.
- Mago, Tools et historique des versions.
- PHP-CS-Fixer, Usage et règles @Symfony.
- Dépôts de PHP_CodeSniffer, Twig CS Fixer, PHP Insights et GrumPHP ; versions publiées de PHPMD.
- Deptrac, dépôt et documentation 4.x ; qossmic/deptrac sur Packagist.
- PhpMetrics, documentation et versions publiées.
- Rector, Composer-based sets, Set lists et rector-symfony.
- Infection, Command line options.
-
Composer,
commande audit
et
configuration
policy. - Symfony, lint:container, lint:twig et lint:yaml ; Doctrine ORM, validation du mapping.
- moon, moon ci, guide CI, VCS hooks et configuration de projet.