Table des matières :
- PHPStan et Rector : périmètre, complémentarité, et ce qu’ils ne feront pas à votre place
- Pré-requis réalistes côté PrestaShop (8.1/9.1) et contraintes d’architecture
- Industrialiser PHPStan : configuration, bootstrap PrestaShop, et extensions utiles
- Réduire le bruit sans tricher : baseline, stubs, et montée en niveau progressive
- Rector : refactoring reproductible, migrations PHP et durcissement progressif des types
- CI/CD : quality gates, exécution déterministe, et feedback rapide en PR
- Cas d’usage concrets sur modules PrestaShop : dette legacy, hooks, et migrations PHP
- Checklist opérationnelle pour rendre PHPStan + Rector “industriels” (et pas juste un POC)
PHPStan et Rector : périmètre, complémentarité, et ce qu’ils ne feront pas à votre place
PHPStan et Rector adressent deux problèmes différents qui se recoupent souvent dans une base de code PHP (dont PrestaShop et ses modules) : détecter des défauts sans exécuter le code, et transformer le code de manière automatisée et reproductible. PHPStan se positionne clairement sur l’analyse statique : le projet revendique explicitement « PHPStan finds bugs in your code without writing tests. » (phpstan.org). Rector, lui, automatise des refactorings et migrations de versions : « Instant Upgrades and Automated Refactoring » (voir la documentation officielle : getrector.org).
Concrètement, PHPStan va vous remonter des erreurs de types, d’appels de méthodes inexistantes, d’arguments incohérents, d’accès à des offsets potentiellement absents, de branches de code impossibles, etc. C’est particulièrement rentable dans l’écosystème PrestaShop, où l’héritage « legacy » (arrays, objets hydratés dynamiquement, classes historiques) génère des bugs silencieux que les tests ne couvrent pas toujours. En revanche, PHPStan ne « corrige » rien : il force une discipline (types, contrats, invariants) et alimente un backlog de dette technique mesurable.
Rector, à l’inverse, est un outil de transformation de code : il applique des règles (rules) et des ensembles (sets) pour moderniser la syntaxe (migration PHP), améliorer la lisibilité, supprimer du dead code, introduire des déclarations de types, ou aligner le code sur des API plus récentes. Son intérêt industriel vient du fait que la transformation est scriptable, rejouable, et donc intégrable dans un pipeline (PRs automatisées, MR, CI). Limite évidente : Rector peut casser des comportements si vos invariants ne sont pas explicités (types, tests, contrats d’API), d’où l’intérêt de coupler Rector à PHPStan (et idéalement à des tests) pour sécuriser la boucle.
Pour cadrer rapidement “qui fait quoi”, une grille simple aide à éviter les attentes irréalistes :
| Besoin | PHPStan | Rector |
|---|---|---|
Détecter des incohérences de types / null |
✅ | ➖ |
| Trouver des appels de méthodes/fonctions inexistants | ✅ | ➖ |
| Migrer une syntaxe PHP (ex. 8.1 → 8.2) | ➖ (détecte les soucis) | ✅ |
| Appliquer des refactorings répétitifs (ex. simplification, dead code) | ➖ | ✅ |
| Garantir le comportement fonctionnel | ❌ (sans tests) | ❌ (sans tests) |
Le point important côté « industrialisation » : les deux outils font gagner du temps si le périmètre est maîtrisé. Sur une base PrestaShop, essayer de “tout analyser / tout refactorer” d’entrée mène souvent à une explosion de bruit (false positives) ou à des modifications trop risquées. Le combo le plus robuste, en pratique : (1) PHPStan sur votre périmètre maîtrisé, (2) Rector sur ce même périmètre, (3) un minimum de tests ou de validation fonctionnelle sur les flux sensibles (commande, paiement, stock).
Pré-requis réalistes côté PrestaShop (8.1/9.1) et contraintes d’architecture
Sur un projet PrestaShop, vous avez rarement un « monorepo » PHP propre : il y a le cœur, des modules, parfois des overrides, un mélange de legacy et de composants Symfony, et des dépendances installées via Composer plus ou moins propres. Avant de parler configuration, il faut cadrer le terrain : PHPStan et Rector tournent en CLI et reposent sur l’autoload Composer. Si votre projet n’a pas de composer.json au root (ou s’il est incomplet), vous devrez soit l’introduire, soit cibler uniquement un module qui embarque son propre vendor/.
Deux patterns courants “qui marchent” dans la vraie vie PrestaShop :
- Pattern A (module autonome) :
modules/MonModule/contient soncomposer.json, sonvendor/, un autoload PSR-4 sursrc/, et des scripts dédiés. Avantage : outillage isolé, reproductible, CI simple. Inconvénient : dépendances dupliquées si vous avez beaucoup de modules. - Pattern B (shop root) : un
composer.jsonà la racine du shop pour outiller plusieurs modules avec un seulvendor/. Avantage : mutualisation. Inconvénient : il faut vraiment contrôler lespathsanalysés, sinon PHPStan/Rector “découvrent” vite des modules tiers, des overrides ou du core que vous n’avez pas envie de gérer.
Côté versions, ne partez pas du principe “PHP 8.x = OK partout”. La compatibilité et les comportements varient (dépréciations, signatures, typage interne, attributs, etc.). Pour cadrer le support des versions PHP sur la branche récente, référez-vous au billet interne PrestaShop 9.1 : compatibilité PHP 8.1–8.5, CLI et nouveautés développeurs : PrestaShop 9.1 : compatibilité PHP 8.1–8.5. Ça conditionne directement les sets Rector (ex. PHP_82, PHP_83) et les niveaux PHPStan (analyse plus stricte avec les nouveautés de typage).
Enfin, il faut intégrer un fait qui fâche : le cœur PrestaShop et beaucoup de modules historiques utilisent des patterns qui sont hostiles à l’analyse statique (propriétés dynamiques, hooks qui injectent des structures non typées, Context global, Tools::getValue() qui renvoie string|array|null|mixed, etc.). L’objectif “industriel” n’est pas de rendre tout le legacy parfaitement typé du jour au lendemain, mais de délimiter un périmètre maîtrisé : par exemple modules/MonModule/src/ (PSR-4) + les couches Symfony/Doctrine que vous contrôlez (voir aussi Symfony PrestaShop & Doctrine).
Un indicateur simple pour choisir le bon périmètre initial : prenez la partie du module qui casse “en prod” quand elle casse (ex. import catalogue, synchronisation ERP, pricing, webhooks), pas forcément celle qui “fait du volume de lignes”. C’est là que l’analyse statique et des refactorings sûrs réduisent le plus la variabilité.
Industrialiser PHPStan : configuration, bootstrap PrestaShop, et extensions utiles
L’installation standard se fait en dépendance de dev :
composer require --dev phpstan/phpstan
composer require --dev phpstan/extension-installer
Sur un module moderne, ajoutez un autoload PSR-4 propre (composer.json) et évitez d’analyser vendor/ et le core complet tant que vous n’avez pas une stratégie de “noise management”. Dans un shop root, la config PHPStan doit explicitement viser des paths limités (ex. modules/monmodule/src, modules/monmodule/classes, voire override/ si vous assumez ce terrain miné).
Le point bloquant le plus fréquent dans PrestaShop : PHPStan ne “comprend” pas votre runtime (constantes, includes historiques, classes chargées par le noyau, container Symfony). La solution pragmatique est d’ajouter un bootstrap pour initialiser l’autoload et quelques constantes minimales, ou au contraire de stubber ce que vous ne pouvez pas charger proprement en CLI. Exemple de phpstan.neon minimaliste (à adapter selon PrestaShop 8.x/9.x et votre layout) :
parameters:
level: 7
paths:
- modules/monmodule/src
bootstrapFiles:
- tools/phpstan/bootstrap.php
tmpDir: var/phpstan
reportUnmatchedIgnoredErrors: true
ignoreErrors:
- '#Call to an undefined method Context::getContext#'
Dans PrestaShop, un bootstrap “trop ambitieux” est souvent contre-productif : inclure config/config.inc.php ou init.php peut déclencher des accès DB, charger des caches, dépendre d’extensions, ou nécessiter des constantes d’environnement. En général, pour de l’analyse statique, vous cherchez plutôt un bootstrap minimal et déterministe (autoload + quelques constantes). Exemple de tools/phpstan/bootstrap.php qui reste volontairement léger :
<?php
declare(strict_types=1);
// 1) Autoload Composer (module autonome ou root shop)
$autoload = __DIR__ . '/../../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
// 2) Constantes parfois attendues par du code legacy
if (!defined('_PS_VERSION_')) {
define('_PS_VERSION_', '0.0.0');
}
// 3) Éviter d'exécuter de la logique runtime (pas de DB, pas de Context réel, etc.)
Enfin, si vous utilisez Doctrine ou des services Symfony dans un module (ce qui devient la norme sur les écrans back-office), activez les extensions correspondantes :
composer require --dev phpstan/phpstan-doctrine
composer require --dev phpstan/phpstan-symfony
Elles réduisent drastiquement les faux positifs sur les repositories, les entités, les types de collections, et l’injection de dépendances. Dans un contexte PrestaShop, ça prend tout son sens dès que vous suivez une architecture de module inspirée de Symfony (container, commandes, controllers) comme détaillé dans le billet Doctrine interne cité plus haut.
Deux détails “qualité industrielle” souvent oubliés :
- Versionner la config (et éviter les options locales non documentées). La reproductibilité est une fonctionnalité.
- Contraindre le runtime de l’analyse : sur certains modules, PHPStan peut consommer beaucoup de mémoire. Lancer l’analyse avec un
memory_limitexplicite (comme dans la checklist en fin d’article) évite les résultats aléatoires selon les machines.
Pour aller plus loin sur les options (sans sur-configurer), la référence officielle PHPStan est utile : PHPStan config reference
Réduire le bruit sans tricher : baseline, stubs, et montée en niveau progressive
Le piège classique, c’est d’activer PHPStan “level max” sur un héritage non typé, puis de finir avec 5 000 erreurs et un outil que personne ne lance. L’approche industrialisable est un ramp-up : vous démarrez à un niveau intermédiaire (5–7), vous fixez les erreurs qui correspondent à de vrais bugs (mauvais appels, null non géré, array offsets), puis vous montez progressivement. Le KPI utile ici n’est pas “0 erreur”, c’est : tendance décroissante et zéro régression (qualité gate en CI).
Une manière pragmatique de rendre la progression visible (et acceptable en équipe) :
- Semaine 1 : niveau 5–6, vous corrigez les erreurs “à forte valeur” (nullability, méthode inexistante, paramètres inversés).
- Semaine 2–3 : vous stabilisez le périmètre (moins de
ignoreErrors, plus de types à l’entrée). - Ensuite : montée vers 7–8 sur les nouvelles couches (
src/), sans chercher à “purifier”classes/legacy si ce n’est pas rentable.
La baseline (--generate-baseline) est un outil acceptable si vous l’utilisez comme une photographie de dette, pas comme un tapis sous lequel on pousse les erreurs. Exemple :
vendor/bin/phpstan analyse --generate-baseline
Ensuite vous verrouillez la CI pour empêcher d’ajouter de nouvelles erreurs, tout en gardant un plan de réduction de la baseline (par dossier, par composant, par sprint). Un schéma efficace en contexte multi-modules : un objectif de réduction “par zone” (ex. src/Domain/ puis src/Infrastructure/) plutôt qu’un objectif global trop abstrait.
Pour du code PrestaShop legacy, vous gagnerez aussi du temps avec des stubs (parameters.stubFiles) pour des classes ou fonctions globales difficiles à charger en CLI. Typiquement : stubs pour Tools, Context, ou des facades maison. L’objectif est d’être strict sur votre code, et d’être pragmatique sur le reste. Exemple de stub minimal (idée : typer le contrat, pas re-définir tout PrestaShop) :
<?php
// tools/phpstan/stubs/Tools.stub.php
class Tools
{
/** @return string|array|null */
public static function getValue(string $key, $default = null);
}
Dernier point : évitez les ignoreErrors trop larges (regex “catch-all”). Préférez des ignores ciblés, avec des IDs d’erreur lorsque possible, et activez reportUnmatchedIgnoredErrors: true pour nettoyer les exceptions obsolètes.
Sur des stacks e-commerce, la robustesse runtime compte : si vous voulez relier qualité statique et stabilité prod, couplez ça à une discipline de gestion d’erreurs PHP (séparation dev/prod, logs structurés), cf. gestion d’erreur PHP — bonnes pratiques.
Rector : refactoring reproductible, migrations PHP et durcissement progressif des types
Rector est particulièrement utile quand vous devez aligner une base de code sur une version PHP cible (8.1→8.2→8.3, etc.), ou quand vous voulez injecter des améliorations mécaniques (typage, simplification) sans y passer des semaines à la main. Ça s’inscrit parfaitement dans la définition canonique du refactoring : « Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. » (Martin Fowler, refactoring.com).
Mise en place standard :
composer require --dev rector/rector
Puis un fichier rector.php (ou rector.php.dist) au root du projet ou du module. Exemple orienté module PrestaShop (périmètre réduit, sets modérés, et pas de magie sur le legacy) :
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/modules/monmodule/src',
]);
// Migration syntaxe + cleanup prudent
$rectorConfig->sets([
SetList::DEAD_CODE,
SetList::CODE_QUALITY,
LevelSetList::UP_TO_PHP_82,
]);
// Évite de toucher aux vendors et aux assets
$rectorConfig->skip([
__DIR__ . '/vendor',
]);
};
Exécutez d’abord en dry-run et générez des diffs lisibles :
vendor/bin/rector process --dry-run
Pour éviter les “grosses PRs illisibles”, une règle simple aide beaucoup : un set = une PR (ou au pire deux sets max si les changements sont liés). Sur un module PrestaShop, cela réduit la charge de revue et facilite les rollbacks.
En pratique, le coupling gagnant est : Rector produit un patch, PHPStan valide que le patch ne dégrade pas le typage et ne crée pas d’incohérences. Si vous avez une couche Doctrine, certains sets Rector (Doctrine/Symfony) peuvent être pertinents, mais ils sont plus “opinionated” : n’activez ces sets que si votre architecture est cohérente (ex. entités, repositories, injection explicite), et que vous êtes prêts à adapter le code généré.
Un réflexe “sécurité” très simple avant d’appliquer Rector sur un module existant :
- vérifier que votre branche est propre (
git status), - lancer Rector une première fois en
--dry-run, - puis lancer Rector en réel, committer immédiatement,
- et seulement après, corriger les éventuelles alertes PHPStan / tests.
Ça évite le “mélange” entre patch automatique et corrections manuelles, et rend la transformation rejouable.
CI/CD : quality gates, exécution déterministe, et feedback rapide en PR
L’industrialisation passe par la CI, pas par la bonne volonté. Le pattern robuste : (1) exécuter PHPStan à chaque PR/MR, (2) exécuter Rector en --dry-run pour refuser les divergences (codebase non conforme), (3) éventuellement proposer une PR automatique Rector (bot) si votre organisation le permet. Pour une approche CI/CD structurée (tests, artefacts, déploiement), le billet interne suivant donne une base solide : Développement PHP critique, architecture DDD, tests et CI/CD.
Exemple GitHub Actions minimal (à adapter à votre runtime PHP, et à votre cache Composer) :
name: quality
on: [pull_request]
jobs:
php-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
tools: composer
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/phpstan analyse --no-progress
- run: vendor/bin/rector process --dry-run --no-progress-bar
Deux points qui évitent des CI “flaky” : fixez des versions d’outils (lockfile), et isolez les chemins analysés (sinon PHPStan va découvrir des anomalies dans des modules tiers ou dans le core, et vous allez finir par désactiver l’outil). Si vous voulez pousser plus loin, vous pouvez générer des rapports machine-readable (--error-format=checkstyle, SARIF) et les publier en artefacts.
Un autre point concret en environnement PrestaShop : verrouillez la version PHP de la CI sur votre cible de prod (ou, si vous avez plusieurs cibles, faites une matrice). Une analyse statique “verte” en 8.3 ne garantit pas que la prod en 8.1 n’aura pas de soucis (et inversement). C’est exactement le genre d’écart que vous cherchez à supprimer avec une boucle CI déterministe.
Enfin, reliez la qualité statique à la qualité runtime. Dans PrestaShop, des erreurs qui “passent” en dev finissent souvent en prod sous forme de lenteurs, de memory bloat, ou d’effets de bords Doctrine (N+1). Même si PHPStan/Rector ne diagnostiquent pas un N+1, ils contribuent à rendre le code plus prédictible (types, contrats) et donc plus simple à profiler. Pour le volet détection runtime, voir : scout & monitoring — détection N+1, memory bloat.
Cas d’usage concrets sur modules PrestaShop : dette legacy, hooks, et migrations PHP
Cas 1 (fréquent) : module historiquement “procedural” qui commence à grossir. Vous introduisez un src/ PSR-4, vous encapsulez les accès Tools::getValue() dans une couche de validation typée (DTO ou value objects), puis vous faites tourner PHPStan sur ce nouveau périmètre. Vous allez immédiatement attraper des incohérences de types sur les hooks (paramètres array dont la forme n’est pas stable) ; la solution réaliste est de documenter ces structures avec des array shapes PHPDoc (ex. @param array{product: Product, id_product: int} $params) et de centraliser la normalisation.
Une variante très efficace en équipe : définir des alias de type PHPStan pour vos hooks “critiques”, afin d’éviter de répéter la forme de l’array partout (et de la modifier en un seul endroit). Exemple :
<?php
/**
* @phpstan-type HookActionProductSaveParams array{
* id_product: int,
* product: \Product
* }
*/
final class HookTypes {}
Puis, dans le handler :
/**
* @param array $params
* @phpstan-param HookActionProductSaveParams $params
*/
public function onActionProductSave(array $params): void
{
// ...
}
Cas 2 : migration technique (ex. alignement PHP 8.2/8.3) sur un module qui tourne encore avec du code 7.x “tolerant”. Rector permet de faire une première passe mécanique (syntaxe, simplifications), puis PHPStan sert de garde-fou pour repérer les endroits où la migration n’est pas mécanique (nullable implicite, accès à des offsets non garantis, appels à des APIs dépréciées). Dans PrestaShop, attendez-vous à traiter explicitement : gestion de null, conversions string|int, et séparations “input non fiable” vs “modèle interne fiable”.
Un mini-scenario typique (vu sur des modules de pricing) : une valeur issue de Tools::getValue('discount') est considérée comme un float, alors qu’elle peut être '', '0', null, voire un array si la requête est malformée. PHPStan vous force à traiter l’entrée comme non fiable, ce qui réduit des bugs “fantômes” en production (et, par ricochet, simplifie les migrations Rector car votre code devient plus explicite).
Cas 3 : overrides et intégration core. Sur le papier, analyser et refactorer les overrides est utile. En pratique, c’est souvent là que l’outillage explose : héritage d’une classe core non typée, méthodes partiellement surchargées, signatures qui ne matchent plus, etc. La stratégie viable : (a) geler les overrides, (b) migrer progressivement vers des mécanismes supportés (services, hooks), (c) n’activer PHPStan/Rector sur les overrides qu’une fois le périmètre réduit. Si vous êtes en trajectoire de migration vers PrestaShop 9, ce sujet rejoint directement la gestion des migrations et compatibilités (voir : PrestaShop 9 — guide de migration).
Checklist opérationnelle pour rendre PHPStan + Rector “industriels” (et pas juste un POC)
Standardisez d’abord la manière de les exécuter : ajoutez des scripts Composer (composer phpstan, composer rector:dry) et documentez le “contrat” attendu (version PHP, extensions, chemins). Sur des équipes multi-projets, ça réduit la friction (tout le monde lance la même commande, dans le même contexte). Exemple :
{
"scripts": {
"phpstan": "php -d memory_limit=1G vendor/bin/phpstan analyse --no-progress",
"rector:dry": "vendor/bin/rector process --dry-run --no-progress-bar"
}
}
Ensuite, outillez la progression. Sur PHPStan : fixez un niveau cible (ex. 7 puis 8), générez une baseline si nécessaire, et mesurez le nombre d’erreurs hors baseline. Sur Rector : versionnez la config, faites des PRs petites (règle/ensemble par règle), et exécutez systématiquement en --dry-run en CI pour éviter que la branche ne dérive. Si vous maintenez plusieurs modules, factorisez la config dans un package interne (ou un dossier partagé) pour éviter les configurations divergentes.
Checklist synthétique (celle qui évite que l’initiative retombe après le POC) :
- [ ] Un périmètre initial clair (souvent
modules/MonModule/src) + exclusion explicite du reste - [ ] Un bootstrap PHPStan minimal (autoload + constantes), sans dépendance DB
- [ ] PHPStan lancé à niveau intermédiaire au départ (objectif : signal > bruit)
- [ ] Une baseline utilisée comme dette versionnée, avec un plan de réduction
- [ ] Des
ignoreErrorsciblés et nettoyés viareportUnmatchedIgnoredErrors: true - [ ] Rector configuré par sets modérés, appliqués en PRs petites
- [ ] CI : PHPStan à chaque PR + Rector en
--dry-run(quality gate) - [ ] Une stratégie “écosystème PrestaShop” explicite : stubs/wrappers pour
Tools,Context, hooks - [ ] Une articulation avec la stabilité runtime (logs/erreurs) : voir aussi gestion d’erreur PHP — bonnes pratiques
- [ ] Un lien avec l’architecture cible (Symfony/Doctrine si pertinent) : Symfony PrestaShop & Doctrine
Enfin, acceptez les limites et formalisez-les. Certains patterns PrestaShop (hooks non typés, context global, legacy classes) imposent soit des stubs, soit des wrappers typés. Tant que ces limites sont explicites, PHPStan et Rector restent extrêmement rentables : ils réduisent les régressions, raccourcissent les revues de code (moins de débats sur la forme), et rendent les migrations de version PHP et PrestaShop plus prévisibles. Si vous cherchez un point de départ pragmatique : ciblez un module critique, isolez src/, montez PHPStan jusqu’au niveau où le signal dépasse le bruit, puis utilisez Rector pour automatiser les refactors que vous répétez déjà à la main.




