Aller au contenu principal
Retour au blog

Mago 1.57 sur Symfony : ce qu’il apporte, où il s’arrête

Installer Mago 1.57 sur un projet Symfony existant : lint, format, guard et analyse en complément de PHPStan, avec config testée, baseline et CI GitHub.

Flavien Métivier · 10 min de lecture
Sommaire · 9 parties

Mago analyse une base WordPress au niveau 9 en 1,1 seconde ; PHPStan 2.3.1 en met 31,2 à froid. Le même Mago remonte pourtant plus de 1 000 erreurs sur un projet où PHPStan au niveau max n’en voit aucune. Entre ces deux chiffres se joue la vraie question : que faire de cet outil Rust sur un projet Symfony en production, sans casser la CI ni se raconter qu’il remplace votre analyseur ?

Mago est une toolchain PHP écrite en Rust — linter, formatter, analyseur statique et guard d’architecture — distribuée en binaire unique, sans PHP requis. La 1.0.0 date du 20 décembre 2025, la 1.57.0 du 14 octobre 2026. Notre verdict tient en une phrase : sur un projet Symfony, Mago complète PHPStan mais ne le remplace pas, car il n’a ni cache ni analyse incrémentale, et ses intégrations Symfony et Doctrine sont des jeux de règles, pas des extensions de types.

Mago, c’est quoi exactement ?

Quatre outils dans un seul exécutable : lint (règles de style, de cohérence et de bonnes pratiques), format (formatter aligné PER-CS par défaut), analyze (inférence de types, à la manière de PHPStan ou Psalm) et guard (frontières d’architecture, le terrain de deptrac). S’y ajoutent init, fix, inspect-baseline et quelques utilitaires. Le projet sort plusieurs versions par semaine : un signe de vitalité, et une première raison d’épingler la version.

Pour un Lead Dev, la bonne question n’est donc pas « Mago remplace-t-il PHPStan ? » mais « quelle partie de ma chaîne qualité peut-il absorber à coût quasi nul ? ». Notre réponse : le formatage, le lint rapide, les règles d’architecture, et l’analyse statique en complément. Mago s’ajoute à un paysage déjà fourni ; notre comparatif des 5 outils d’audit PHP aide à situer chacun.

Installer Mago et épingler la version

Sur un projet Symfony, la voie la plus sûre est Composer en dépendance de dev : le composer.lock fige la version pour toute l’équipe et pour la CI. Le paquet télécharge le binaire depuis GitHub ; en CI, exposez un GITHUB_TOKEN pour éviter le rate limit.

bash
# Dépendance de dev : le lock fige la version exacte
composer require --dev "carthage-software/mago:^1.57.0"
vendor/bin/mago --version   # mago 1.57.0

# Alternative sans PHP : l’image Docker, avec un tag précis
docker run --rm -v "$PWD":/app -w /app ghcr.io/carthage-software/mago:1.57.0 lint

Piège de terrain : le tag Docker :1 de la recette officielle flotte sur toute la 1.x. Avec plusieurs releases par semaine, une nouvelle règle activée par défaut peut faire passer au rouge une branche que personne n’a touchée. En CI, préférez :1.57.0 et montez de version par une PR dédiée. Autre détail : l’image ne contient ni PHP ni Composer, donc pour analyze il faut un vendor/ installé, sans quoi les types des dépendances restent inconnus.

Un mago.toml pragmatique pour Symfony

mago init génère un premier fichier, mais Mago ne lit ni phpstan.neon ni psalm.xml : la configuration est entièrement séparée, et vous maintiendrez deux fichiers pendant la cohabitation. Voici une base défendable pour une application existante. Nous l’avons validée avec le binaire 1.57.0 sur un projet de test : les quatre commandes la lisent sans erreur.

bash
version = "1"
php-version = "8.3"            # alignez sur config.platform.php de composer.json

[source]
paths    = ["src", "tests"]
includes = ["vendor"]          # indexé pour résoudre les types, pas analysé
excludes = ["var/**", "migrations/**"]

[formatter]
print-width = 120

[linter]
integrations       = ["symfony", "phpunit"]
baseline           = "lint-baseline.toml"
baseline-variant   = "loose"
minimum-fail-level = "error"

[analyzer]
baseline         = "analysis-baseline.toml"
baseline-variant = "loose"
check-throws     = false        # valeur par défaut, voir plus bas

Deux réglages méritent une explication. minimum-fail-level = "error" fait que les avertissements s’affichent sans faire échouer la commande : sur notre projet de test, mago lint signalait deux warnings et sortait en code 0. Et la variante loose de la baseline compte les problèmes par triplet fichier/code/message, ce qui la rend tolérante aux décalages de lignes — exactement ce qu’on veut sur un code qui bouge tous les jours.

Ce qu’il faut comprendre sur integrations = ["symfony", "phpunit"] : cela active des règles de lint dédiées (contrôleurs final, préférence pour les interfaces…), pas une compréhension du container Symfony ou des repositories Doctrine. Rien d’équivalent à phpstan-symfony ou phpstan-doctrine n’est documenté à ce jour.

Adopter Mago sur un projet existant sans bloquer l’équipe

Règle de base : jamais d’outil bloquant sur du code existant sans état zéro maîtrisé. La baseline fige la dette actuelle ; le nouveau code, lui, doit être propre. Avant d’activer quoi que ce soit, fixez un point de départ chiffré avec cinq métriques pour mesurer la qualité d’un code PHP : vous saurez ensuite si l’outil fait bouger les bons indicateurs.

bash
# 1. Figer la dette existante (lint puis analyze)
vendor/bin/mago lint --generate-baseline --baseline lint-baseline.toml
vendor/bin/mago analyze --generate-baseline --baseline analysis-baseline.toml

# 2. Voir où se concentre la dette, par règle
vendor/bin/mago inspect-baseline lint-baseline.toml --group code --limit 20

# 3. Formatter : un commit isolé, rien d’autre dedans
vendor/bin/mago format
git commit -am "style: mago format"
git rev-parse HEAD >> .git-blame-ignore-revs

# 4. Plus tard : purger les entrées devenues obsolètes
vendor/bin/mago lint --remove-outdated-baseline-entries --baseline lint-baseline.toml
  • Format d’abord : diff purement mécanique, un seul commit, référencé dans .git-blame-ignore-revs pour ne pas polluer git blame. Attention, le formatter est opinionated (120 colonnes, style fixe) : ce n’est pas une migration transparente depuis une config PHP-CS-Fixer composée à la main.
  • Lint ensuite, avec baseline : la dette est figée, toute nouvelle violation bloque. mago lint --fix n’applique que les correctifs sûrs ; les options --potentially-unsafe et --unsafe élargissent le périmètre — relisez le diff.
  • Analyze en dernier, en observation : comparez ses trouvailles avec celles de PHPStan avant d’en faire un gate.
  • La baseline ne peut que décroître : un contrôle de CI qui échoue si le nombre d’entrées augmente dans une PR évite qu’elle devienne un dépotoir. C’est un cliquet, pas une poubelle.

Mago et PHPStan dans la même CI GitHub Actions

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Calculer mon audit →

Mago détecte la variable GITHUB_ACTIONS et bascule seul sur le format github depuis la 1.17.0 : les problèmes apparaissent en annotations directement dans la PR. D’autres formats existent (gitlab, checkstyle, sarif, json). Le workflow ci-dessous, adapté de la recette officielle, garde PHPStan dans un job séparé : retour parallèle, et chaque outil peut évoluer sans toucher à l’autre.

yaml
name: Quality

on:
  pull_request:
  push:
    branches: [main]

jobs:
  mago:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Format
        run: vendor/bin/mago format --check
      - name: Lint
        if: success() || failure()
        run: vendor/bin/mago lint
      - name: Guard
        if: success() || failure()
        run: vendor/bin/mago guard

  phpstan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/phpstan analyse --no-progress

Trois détails comptent. En CI, utilisez mago format --check, l’option que la doc réserve à cet usage : elle sort en code 1 dès qu’un fichier doit changer. Le if: success() || failure() fait tourner chaque étape même si la précédente échoue, pour voir tous les problèmes en un seul passage. Enfin, analyze n’est pas dans le job tant qu’il n’a pas fait ses preuves : plutôt que de le rendre non bloquant par un continue-on-error, lancez-le en local ou dans un job non requis par la protection de branche. Mago ne dispense pas non plus de bien régler PHPStan : voici comment monter PHPStan jusqu’au niveau 10 sans bloquer la CI.

Guard : vos règles d’architecture dans le même binaire

Le guard est la partie la plus sous-estimée de Mago. Il couvre deux familles de règles : perimeter (qui a le droit de dépendre de qui) et structural (conventions sur les classes). L’exemple ci-dessous interdit au domaine toute dépendance hors PHP natif et impose des contrôleurs final suffixés Controller. Testé sur la 1.57.0 : un use Symfony\Component\HttpFoundation\Request dans App\Domain lève une erreur disallowed-use, et mago guard sort en code 1.

bash
[guard]
mode = "default"               # perimeter + structural
minimum-fail-level = "error"

[guard.perimeter]
layering = ["App\\Domain", "App\\Application", "App\\Infrastructure"]

[guard.perimeter.layers]
core      = ["@native"]
framework = ["Symfony\\**", "Doctrine\\**"]

[[guard.perimeter.rules]]
namespace = "App\\Domain\\"     # un namespace se termine par \\
permit    = ["@layer:core", "@self"]

[[guard.structural.rules]]
on            = "App\\Controller\\**"
target        = "class"
must-be-named = "*Controller"
must-be-final = true
reason        = "Les contrôleurs sont final et suffixés Controller."

Si vous utilisez déjà deptrac, rien ne presse : le gain est d’avoir un outil de moins à installer, pas une capacité nouvelle. Sur un projet neuf, en revanche, le guard évite d’ajouter une dépendance de plus.

Mago est-il plus rapide que PHPStan ?

Oui à froid, beaucoup moins à chaud. Les mesures indépendantes publiées en 2026 donnent ceci :

  • WordPress, niveau 9, à froid : Mago 1.54.0 en 1,1 s ; PHPStan 2.1.49 en 48,6 s et 2.3.1 en 31,2 s (benchmarks scode.nl et Sander Muller).
  • Même base, commit moyen, cache chaud : Mago 1,1 s, PHPStan 2.3.1 1,8 s.
  • Projet de 3 000 fichiers, niveau max : Mago 1.0.2 en 1,73 s ; PHPStan 6,4 s à froid mais 0,27 s avec cache (supun.io).

Lecture : l’écart spectaculaire existe là où personne ne travaille — PHPStan à froid. Mago n’a ni cache ni analyse incrémentale : il relance tout à chaque exécution. Sur la boucle quotidienne d’un gros monolithe, un PHPStan bien caché peut être plus rapide. Le gain réel est sur le lint et le format, en pre-commit (mago format --staged, mago lint --staged) et en CI sans cache. Notez aussi que ces mesures portent sur des versions antérieures à la 1.57 : refaites-les sur votre code.

Où Mago s’arrête

  • Le bruit sur une base existante. Le test supun.io (Mago 1.0.2) a remonté plus de 1 000 erreurs là où PHPStan max en signalait 0, dont environ 500 liées à check-throws. Ce n’est pas forcément du faux positif — c’est un autre jeu de règles — mais le tri coûte. check-throws est désactivé par défaut : laissez-le ainsi tant que la baseline n’est pas en place.
  • Le code généré. Le même retour signale des soucis avec les proxies Zenstruck Foundry. Excluez var/ et le code généré, et traitez les écarts par baseline plutôt que par désactivation globale.
  • Les types Symfony et Doctrine. Aucune documentation sur un support des generics de repositories ou du container : ne présumez rien, mesurez.
  • La maturité. PHP Architect, sur la 1.43, le décrit comme « still maturing », à utiliser en « yes and » avec les outils existants. La FAQ annonce un LSP officiel « planned for 2.0.0 », information peut-être datée.
  • La gouvernance. Le projet repose sur un mainteneur principal, Seifeddine Gmati, soutenu par des sponsors dont CHECK24 et JetBrains. La dépendance à une personne est notre lecture, pas un chiffre publié : intégrez-la à votre analyse de risque.

Verdict : un plan d’adoption en quatre semaines

  • Semaine 1 : mago init, baselines lint et analyze, job CI non requis.
  • Semaine 2 : commit unique de formatage, .git-blame-ignore-revs, format --check requis.
  • Semaine 3 : lint requis avec baseline, guard sur deux ou trois règles de couches à forte valeur.
  • Semaine 4 : bilan. Comparez analyze et PHPStan sur vos PR ; si Mago n’apporte rien de plus, gardez format, lint et guard, et laissez l’analyse à PHPStan.

Gardez PHPStan comme gate de types et placez Mago là où sa vitesse compte vraiment. Épinglez la version, refaites le test à chaque minor : ce qui bloque aujourd’hui — cache, extensions de types — peut tomber demain, vu le rythme du projet.

Ajouter Mago à côté de PHPStan prend une demi-journée. Décider quoi faire des centaines de signalements qui remontent — lesquels masquent un vrai risque, dans quel ordre les traiter — prend plus de temps et demande du recul sur le code. C’est précisément le travail d’un Bear Scan : un état des lieux chiffré et priorisé de la qualité d’une base Symfony, en dix jours.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.