Table des matières :
- Dette technique Symfony : ce qui se mesure, et ce qui n’est que ressenti
- Préparer un profilage Blackfire exploitable (Symfony 6.4 / PHP 8.2)
- Lire un profil Blackfire sur Symfony : flamegraph utile, pas décoratif
- Transformer un profil en refactoring mesurable : 4 patterns qui remboursent vraiment
- Mettre Blackfire au service d’une dette “pilotée” : budgets, CI, comparaisons
- Plan d’exécution : un sprint dette technique Symfony “qui rembourse”
Dette technique Symfony : ce qui se mesure, et ce qui n’est que ressenti
La dette technique Symfony n’est pas une notion morale, c’est un écart objectivable entre ce que votre code fait aujourd’hui et ce qu’il vous coûtera demain (en temps CPU, en risques de régression, en effort de maintenance). Ward Cunningham, qui a popularisé le terme, résume l’idée ainsi : « Shipping first time code is like going into debt. » (Cunningham, OOPSLA’92 — The WyCash Portfolio Management System). Dans un projet Symfony (et encore plus dans un contexte hybride type PrestaShop 8/9 où cohabitent legacy et Symfony), cette dette se manifeste rarement par “du code moche” ; elle apparaît surtout sous forme de latences, d’explosions de requêtes SQL, de dépendances cycliques, de services instanciés à répétition, et de caches invalidés trop souvent.
Une manière utile de la rendre tangible est de distinguer :
- Le principal : ce que vous “devez” déjà (couplage, choix d’ORM, conventions, dette de tests).
- Les intérêts : ce que vous payez à chaque exécution (CPU, wall time, I/O, DB) et à chaque changement (régressions, lenteur des reviews, peur de toucher).
La conséquence pratique : si vous ne mesurez pas, vous refactorez à l’aveugle. Donald Knuth rappelait déjà : « Premature optimization is the root of all evil. » (Structured Programming with go to statements, 1974). Le corollaire moderne est simple : l’optimisation “non prématurée” commence par un profilage reproductible. Dans Symfony, on ne parle pas seulement de “temps de réponse”, mais de répartition wall time vs CPU, temps SQL, allocations mémoire, I/O réseau, boot du kernel, compilation/chargement du container, rendu Twig, etc.
Avant de sortir Blackfire, fixez des KPIs orientés refactoring mesurable. Exemple de base (HTTP) : p95 “wall time” (ou TTFB côté serveur), CPU time, peak memory, nombre de requêtes SQL, temps cumulé SQL, taux de cache-hit (opcache, HTTP cache, cache applicatif). Exemple “batch” (console Symfony) : durée totale, CPU, I/O disque, appels réseau.
- Pilotez en percentiles (p95/p99), pas en moyenne : les utilisateurs (et l’admin en BO) ressentent les lenteurs “au pire moment”. La littérature SRE popularise cette approche de pilotage par SLO/percentiles (Google, Site Reliability Engineering, 2016).
- Verrouillez un KPI primaire + 2 ou 3 secondaires : sinon vous “gagnez” 50 ms en wall time… en doublant la RAM ou en explosant le SQL.
Pour une méthode d’audit plus large (serveur + PHP + SQL), vous pouvez vous appuyer sur une méthode d’audit performance PrestaShop en 6 étapes, reproductible.
Checklist KPI (simple, mais exploitable) à formaliser avant toute PR de refactor :
| Type | KPI principal | KPI secondaires (exemples) | Pourquoi c’est utile |
|---|---|---|---|
| HTTP BO (listing) | p95 wall time | SQL count, SQL time, CPU, peak memory | Le BO souffre vite du N+1 et du rendu |
| HTTP API | p95 wall time | CPU, I/O externe, taille payload | Le “ressenti” front dépend du TTFB |
| CLI (import/reindex) | durée totale | CPU, mémoire, I/O disque, appels réseau | Les batchs coûtent en infra + fenêtres de traitement |
Préparer un profilage Blackfire exploitable (Symfony 6.4 / PHP 8.2)
Contexte recommandé (2026, réaliste en production) : PHP 8.2 ou 8.3, Symfony 6.4 LTS, OPcache activé, et un runtime proche prod (mêmes extensions, même mode, mêmes caches). Dans PrestaShop 9, cette base Symfony 6.x est typique pour le back-office moderne et les briques API. Pré-requis côté Blackfire : un compte Blackfire, un Agent qui tourne sur le serveur (ou dans le cluster), et l’extension PHP blackfire sur les workers FPM/LSAPI. Attention : Blackfire introduit une surcharge contrôlée pendant le profilage ; n’enclenchez pas ça en plein checkout sans scénario maîtrisé (et idéalement sans données sensibles).
Point bloquant fréquent : le bruit de mesure. Pour obtenir un signal utile, vous devez neutraliser tout ce qui déforme le profil :
- Désactiver Xdebug sur l’environnement profilé (Xdebug modifie drastiquement le timing). Gardez-le pour le débogage local, cf. configurer Xdebug dans VS Code pour déboguer du PHP en local.
- APPENV=prod et APPDEBUG=0 (sinon, votre profil mesure surtout la stack de debug).
- Warmup : cache Symfony déjà généré, routes et conteneur compilés, OPcache “chaud”.
- Dataset comparable : un BO “admin connecté sur un catalogue de 20 produits” n’a presque aucune valeur si la prod en a 200 000.
Pour OPcache, vérifiez non seulement l’activation, mais aussi les paramètres qui impactent la stabilité des profils (taille mémoire, nombre max de scripts, timestamps) via activer et vérifier OPcache (cPanel) et ses réglages. En pratique, des profils incohérents viennent souvent d’un OPcache sous-dimensionné (évictions) ou d’un environnement qui recompilera “au hasard” (fichiers modifiés, timestamps).
L’objectif n’est pas d’avoir “un profil”, mais un profil comparable (avant/après). Blackfire permet de profiler une requête HTTP ou une commande CLI, par exemple :
# HTTP
blackfire curl -H 'Host: shop.example' 'https://127.0.0.1/admin/products'
# CLI (batch)
blackfire run php bin/console app:reindex --env=prod --no-debug
Deux pratiques qui rendent vos profils beaucoup plus actionnables :
- Répéter 3 à 5 fois et garder la médiane (et pas “le meilleur run”). En e-commerce, la variance vient vite (cache, réseau, DB).
- Figer le scénario : même compte admin, même filtre, même pagination, même options activées. Pour une page de grille produits, documentez par exemple : page=1, tri=référence, filtre=actif, 50 items.
Formalisez aussi vos scénarios (pages BO, endpoints API, batchs import/export) et isolez-les. Sur un site e-commerce, évitez de mélanger l’impact d’un cache reverse-proxy, d’un cache applicatif et de la base de données dans une seule prise : vous voulez savoir où vous remboursez la dette.
Pour la partie SQL pure (expliquer une latence, activer slow query log, index), les articles activer et analyser le debug/profiling SQL côté PrestaShop et activer le slow query log MySQL pour traquer les requêtes lentes sont un bon complément.
Si vous devez partager un profil (interne, prestataire, équipe), évitez d’y faire apparaître des identifiants ou paramètres sensibles : privilégiez une URL de staging, des comptes techniques et un jeu de données anonymisé.
Pour le “comment ça fonctionne” et les métriques disponibles, la documentation Blackfire est une référence utile.
Lire un profil Blackfire sur Symfony : flamegraph utile, pas décoratif
Blackfire vous donne plusieurs vues (timeline, call graph, hotspots). Le point qui évite les mauvaises décisions : distinguer wall time (temps total ressenti) et CPU time (temps réellement consommé par le CPU).
- Une régression wall time sans hausse CPU signifie souvent : I/O réseau (API tierce), disque, attente MySQL, verrouillage, contention, ou throttling (PHP-FPM saturé, WaitQ côté LiteSpeed, etc.).
- À l’inverse, une hausse CPU pointe vers du code PHP, du parsing Twig, de la sérialisation, ou des boucles.
Dans Symfony, les “gros blocs” typiques que vous verrez ressortir sont : HttpKernel::handle() (pipeline), l’empilement d’event listeners/subscribers, la résolution de contrôleur, la construction de la réponse, puis Twig et Doctrine.
Méthode de lecture simple (et très efficace) pour éviter le “flamegraph tourism” :
- Repérez le dominant : le top 1 ou top 2 en temps inclusif (wall et/ou CPU).
- Vérifiez si c’est structurel (boot kernel, container) ou spécifique au parcours (SQL, template, API).
- Descendez jusqu’au premier appel “métier” : le moment où on quitte les couches framework pour arriver à votre code (Repository, service, normalizer, template).
- Reliez à un symptôme mesurable : SQL count, taille des collections hydratées, boucles, appels HTTP.
Un profil “sale” affichera souvent beaucoup de temps dans la couche d’abstraction (hydration ORM, normalizers, templating) alors que le vrai problème est fréquemment une requête. Doctrine peut paraître coûteux en PHP, mais la cause racine est fréquemment une requête trop large, un tri côté PHP au lieu du SQL, ou un N+1.
Mini-scénario réaliste (BO) : une grille produits qui charge 50 lignes mais, pour chaque produit, va chercher séparément stock + images + catégories. Visuellement, le flamegraph vous montre “Doctrine + hydration” partout. En réalité, l’action à fort ROI est de réduire le nombre de round-trips DB (ou de construire un read model dédié) plutôt que de micro-optimiser l’hydratation.
Pour un rappel “structure Symfony côté PrestaShop 9”, voir structure d’un module PrestaShop 9 (services, container, bonnes pratiques Symfony).
Pièges classiques à neutraliser avant d’interpréter : (1) profil à froid (cache Symfony non warm, OPcache vide), (2) debug toolbar / profiler Symfony activés, (3) requêtes non représentatives (admin connecté avec un catalogue minuscule), (4) données incohérentes (fixtures vs prod). Les comparaisons avant/après n’ont de valeur que si la charge et le dataset sont proches.
En e-commerce, la taille du catalogue et la cardinalité des associations font exploser les chemins “invisibles” en dev. À ce stade, il vaut mieux faire une passe d’observabilité serveur (FPM, MySQL, cache) ; si vous êtes sous LiteSpeed/LSAPI, l’analyse de la file d’attente et de la saturation se recoupe bien avec LSAPI, opcache et suivi WaitQ pour diagnostiquer une saturation PHP.
Transformer un profil en refactoring mesurable : 4 patterns qui remboursent vraiment
Le pattern n°1, le plus rentable : N+1 SQL et chargement paresseux non maîtrisé. Symptômes Blackfire : temps important dans Doctrine (hydration), nombre d’appels executeQuery, et wall time dominé par SQL.
Correctifs typiques (par ordre de “ROI / risque”) :
- Requêtes dédiées (Repository) : au lieu d’un graphe d’objets, récupérer exactement les champs nécessaires au listing.
- Eager loading ciblé (
JOIN FETCH) quand vous savez que l’association sera utilisée dans la vue (attention à l’explosion de lignes si vous joignez trop). - Read models pour grilles/listings : DTO hydratés par requête SQL/DBAL (utile en BO, exports, endpoints API).
- Pagination stricte + index adaptés : un tri + filtre sur colonnes non indexées peut suffire à “pourrir” un p95.
Mesure attendue : chute du nombre de requêtes (ex : 120 → 12), baisse du temps SQL cumulé (ex : 180 ms → 40 ms) et stabilisation du p95. Pour les aspects indexation/schémas, utilisez une grille de lecture MySQL “prod” comme dans optimiser requêtes et schémas MySQL en production. Et gardez une règle : un refactoring Doctrine “propre” qui n’a pas fait bouger SQL count / SQL time est rarement celui qui rembourse.
Le pattern n°2 : services instanciés en boucle / anti-pattern Service Locator. Typiquement dans des handlers, normalizers, ou modules PrestaShop qui tirent des services via le container dans une boucle de produits (ou via un adapter legacy). Symptômes : beaucoup de temps dans la résolution du container, les factories, et des objets “jetables”.
Correctif : (a) injection explicite (constructor injection) + services immuables, (b) pré-calcul en amont (ex : map d’IDs), (c) éviter la construction de gros graphes d’objets pour un simple listing, (d) mettre en cache au bon endroit (cache applicatif sur données stables, pas sur objets Doctrine attachés).
Mesure : baisse CPU + baisse allocations mémoire ; vous cherchez moins un “gain de 20 ms” qu’un profil qui devient stable et lisible. Indice utile : si votre flamegraph montre une “mosaïque” de petites instanciations au lieu de quelques blocs bien identifiés, votre dette est souvent du côté des dépendances et de la construction d’objets.
Le pattern n°3 : Twig et rendu serveur trop coûteux (surtout back-office). Symptômes : hot spots Twig, filtres/extends multiples, composants répétitifs, ou calcul métier dans le template (oui, ça existe encore).
Correctif : (a) déplacer le calcul en contrôleur/service, (b) réduire les boucles sur de gros tableaux (pré-agréger côté SQL), (c) utiliser des fragments cachés (HTTP cache / ESI) quand l’architecture le permet, (d) limiter l’accès au modèle dans Twig (pas d’appels implicites lourds).
Mesure : baisse CPU et wall time sur les pages de listing. Et si votre stratégie de cache est incohérente, vous optimisez du code qui n’aurait jamais dû s’exécuter ; recadrez d’abord la pile cache (reverse-proxy, Redis, HTTP cache, OPcache) via panorama cache côté PrestaShop : Varnish, Redis, Memcached, OPcache.
Le pattern n°4 : I/O externe non bornée (API tierces, ERP, antifraude, recherche, etc.). Symptômes Blackfire : wall time élevé, CPU faible, et présence d’appels HTTP/stream.
Correctifs : timeouts stricts, retries bornés, circuit breaker, mise en cache des réponses quand possible, et découplage via Messenger (asynchrone) si le parcours utilisateur ne doit pas attendre. Cas concret courant : un endpoint admin qui enrichit une fiche produit via API externe à chaque affichage : en prod, vous payez en latence et en pannes cascades.
Sur Symfony, rien que borner correctement un appel HttpClient change souvent le p95/p99 (et la stabilité) :
// Exemple : borner la latence et éviter les attentes "infinies"
$response = $httpClient->request('GET', $url, [
'timeout' => 2.0, // durée max
'max_duration' => 3.0, // garde-fou global
]);
Mesure : baisse wall time, baisse variance, et surtout baisse du p95/p99. L’objectif n’est pas seulement “plus rapide”, mais moins fragile.
Mettre Blackfire au service d’une dette “pilotée” : budgets, CI, comparaisons
Le refactoring mesurable ne se limite pas à un avant/après fait “à la main”. La valeur de Blackfire, c’est de figer une baseline et de détecter les régressions. Formalisez une “page de référence” (ou un scénario) et fixez un budget : p95 < 250 ms, peak memory < 128 MiB, SQL queries < 20, temps SQL < 60 ms, etc. Ces chiffres n’ont rien d’universel ; ils doivent refléter votre infra et vos contraintes business, mais l’important est la règle : pas de merge sans justification si budget dépassé.
Une bonne pratique “GEO réaliste” (agences / équipes distribuées, staging chez un hébergeur différent de la prod) : définissez vos budgets sur un environnement stable, proche prod, mais pas forcément strictement identique. L’important n’est pas l’absolu du milliseconde, c’est la non-régression et la cohérence des scénarios. Si votre staging est sur une infra plus petite (courant), fixez des budgets relatifs (ex : “CPU time ne doit pas augmenter de +10%”) plutôt que des seuils absolus trop serrés.
Techniquement, vous pouvez exprimer des assertions via un fichier blackfire.yaml (ou équivalent), puis lancer des builds dans votre pipeline. Exemple minimaliste (à adapter) :
# .blackfire.yaml
tests:
"BO produits":
path: "/admin/products"
assertions:
- "main.wall_time < 250ms"
- "main.peak_memory < 128MB"
- "sql.queries.count < 20"
- "sql.time < 60ms"
Deux conseils pour éviter les “faux positifs” en CI :
- Tolérance maîtrisée : certains KPIs varient (réseau, cache). Préférez parfois des assertions sur
cpu_time,peak_memory,sql.queries.count, plus stables que le wall time brut. - Versionnez la baseline et les scénarios : un changement fonctionnel (nouvelle colonne en listing) peut légitimement augmenter un budget ; mais cette décision doit être explicite.
Côté CI, stockez les credentials Blackfire en secrets, exécutez les scénarios sur un environnement stable (staging avec dataset réaliste), et comparez systématiquement la PR à la baseline du branch principal. À ce stade, Blackfire n’est pas un APM temps réel ; c’est un outil de profilage et de non-régression.
Pour l’exploitation en continu (saturation, erreurs, corrélation infra), vous devez compléter avec du monitoring et des tests de charge ; l’article monitoring, tests de charge et runbooks (pics type soldes) donne un cadre exploitable côté runbooks.
Plan d’exécution : un sprint dette technique Symfony “qui rembourse”
Commencez par choisir 2 à 4 parcours qui comptent réellement : une page BO lente (grille produits/commandes), un endpoint API consommé par le front/headless, un batch (sync ERP, indexation), et éventuellement une page FO non cachée. Faites 3 profils par parcours (médiane) sur un environnement stable. Conservez les traces “avant” et annotez ce que vous voyez : temps kernel boot, nombre de listeners, temps SQL, hotspots Twig, appels réseau. Si vous travaillez dans l’écosystème PrestaShop, n’oubliez pas que certaines lenteurs viennent du legacy (class Db, ObjectModel, hooks) ; profilage et refactoring doivent accepter cette réalité au lieu de la nier.
Plan de sprint simple (et très défendable en comité produit) :
- Jour 1 : baseline
- scénarios figés (URL, compte, dataset, options)
- KPIs cibles + budgets
- 3 à 5 runs, médiane, sauvegarde des profils
- Jour 2 : correctif minimal
- 1 pattern à fort ROI (souvent N+1 ou I/O externe)
- changement limité, testable, réversible
- Jour 3 : preuve + verrouillage
- re-profilage, comparaison avant/après
- ajout d’assertions Blackfire en CI
- note “runbook” : symptôme → cause → fix → KPI attendu
Ensuite, ne refactorez pas “large”. Choisissez un pattern à fort ROI (souvent N+1 ou I/O externe), implémentez un correctif minimal, et re-profilez immédiatement. C’est là que la dette devient mesurable : si vous n’obtenez pas d’amélioration nette sur le KPI ciblé, c’est que votre hypothèse était mauvaise (ou que le cache masquait le problème).
Dans 30% des cas, la meilleure optimisation est un cache bien positionné et invalidé proprement ; dans 30% c’est une requête ; dans 30% c’est un anti-pattern d’architecture ; le reste, c’est de l’infra (workers insuffisants, OPcache mal dimensionné, DB saturée). L’intérêt de cette répartition “terrain”, c’est qu’elle force à tester l’hypothèse plutôt que de débattre.
Enfin, verrouillez le gain : assertions Blackfire en CI, baseline versionnée, et documentation “runbook” avec le pourquoi (pas juste le quoi). Ajoutez une règle simple dans vos PR : toute modification touchant contrôleurs/services Repository/Twig sur les parcours critiques doit venir avec un profil (ou un test de non-régression). Ce n’est pas bureaucratique : c’est le seul moyen d’empêcher la dette technique Symfony de revenir par petites touches, sprint après sprint.
Pour les sujets connexes (sécurité/perf dans le tunnel), gardez une séparation stricte : un WAF ou un durcissement ne remplace pas un profilage, et inversement ; si vous touchez au checkout, faites-le avec des garde-fous, cf. réduire les faux positifs WAF sans casser le checkout PrestaShop.
