Votre rector.php date probablement de 2023 : une liste de SetList copiée d’un projet à l’autre, des constantes PHP figées, et une migration Symfony qu’on repousse parce qu’elle produirait quatre cents fichiers modifiés d’un coup. Rector 2.7 (6 octobre 2026) règle une bonne partie du problème : les règles s’activent désormais selon les versions réellement installées dans votre projet. Voici la configuration d’un projet Symfony en 2026, ce qui a changé, et une méthode pour avancer par petits diffs relisibles — tout a été exécuté sur un squelette Symfony 7.4, puis 8.1.
Si vous découvrez l’outil, commencez par les bases de Rector : installation, règles, premier passage. Cet article suppose un rector.php existant.
Ce qui a changé entre Rector 2.6 et 2.7
- 2.6.0 (3 août) introduit les sets basés sur Composer.
- 2.6.5 (30 août) déprécie les constantes
SetList::PHP_*et rend l’invalidation du cache plus fine. - 2.6.6 et 2.6.7 (septembre) ajoutent
--filter,--max-changes,--onlyrépétable et la commandevalidate-config. - 2.7.0 (6 octobre) apporte de nouvelles règles, dont
ConstructorReadonlyAssignToDefaultRector, et passe sur PHPStan 2.3.
Trois changements, un seul effet : la configuration devient déclarative et suit votre projet. La cible tient en une dizaine de lignes :
<?php
// rector.php : Rector 2.7.0, Symfony 7.4, PHP 8.4+
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
// règles liées aux versions réellement installées
->withComposerBased(doctrine: true, phpunit: true, symfony: true, twig: true)
// annotations @Route -> attributs #[Route]
->withAttributesSets(symfony: true)
// sans argument : lit la contrainte "php" de composer.json
->withPhpSets()
->withPreparedSets(deadCode: true, codeQuality: true)
// montée progressive : relevée PR après PR
->withTypeCoverageLevel(20)
->withImportNames(removeUnusedImports: true);withComposerBased : laisser les versions installées choisir les règles
withComposerBased() active dans Rector les règles qui correspondent aux versions de Symfony, Doctrine, PHPUnit et Twig réellement installées. Avant, monter de Symfony 6.4 à 7.x consistait à empiler des sets versionnés à la main, avec le risque d’appliquer une règle de migration à un projet qui n’était pas encore sur la bonne version. Désormais, chaque règle porte une contrainte — par exemple symfony/console >=7.4 — que Rector compare à la version installée. Pas de montée de version, pas de règle qui anticipe.
La commande rector composer-based explique quelles règles sont actives, et pourquoi. Sortie réelle sur notre squelette Symfony 7.4.21 avec Doctrine ORM 3.7.5, PHPUnit 11.5.57 et Twig 3.30 :
$ vendor/bin/rector composer-based
Rule Package Requires Installed Active
CastDoctrineExprToStringRector doctrine/orm >=3.0 3.7.5.0 yes
AnyMatcherToNewAnyInvokedCountRector phpunit/phpunit >=12.5 11.5.57.0 no
ChangeCollectionTypeOption...Rector symfony/form >=2.7 - no
...
! [NOTE] 213 of 370 composer package bound items are activeUn « no » a deux causes possibles : le paquet est absent (-), ou la contrainte dépasse la version installée — les règles PHPUnit 12.5 s’activeront d’elles-mêmes le jour où vous monterez. C’est la propriété recherchée : le même rector.php sert avant et après la montée de version. Pour une PR qui ne contient que ces règles, rector process --composer-based les isole. Les détails propres à PHPUnit sont dans notre article sur la migration vers PHPUnit 13.
Une limite : les notes de la 2.6.0 indiquent que PHPUnit et Symfony ont été migrés en premier et que la couverture complète est en cours. Et withComposerBased(symfony: true) seul n’a pas converti nos annotations @Route : il a fallu withAttributesSets(symfony: true), qui a produit #[Route(path: '/invoices', name: 'invoice_index', methods: ['GET'])]. Côté Doctrine, doctrine: true a bien converti @ORM\Entity et @ORM\Column en attributs.
withPhpSets() remplace SetList::PHP_*
Depuis la 2.6.5, les constantes SetList::PHP_82, PHP_83… sont dépréciées. Elles fonctionnent encore en 2.7.0, mais chaque exécution affiche un avertissement : « The per-version PHP set is deprecated. Use "withPhpSets()" or "withPhpLevel()" instead ». Les méthodes withPhp53Sets() à withPhp74Sets() sont dépréciées aussi.
withPhpSets() accepte un flag par version, de php53 à php86 : php85 et php86 existent bien en 2.7.0. Trois règles, lues dans le code de Rector : un seul appel par configuration, un seul flag à la fois — c’est un plafond, les versions inférieures sont incluses —, et de préférence aucun argument, auquel cas Rector lit la contrainte php de composer.json.
Le piège que nous avons rencontré : avec "php": ">=8.2" dans composer.json, withPhpSets(php86: true) n’a pas réécrit min(10, max(0, $v)). Avec "php": ">=8.6", la règle MinMaxToClampRector du set 8.6 a produit clamp($v, 0, 10). Le flag explicite ne dépasse pas la plateforme déclarée : montez d’abord require.php. Pour choisir cette cible avant la fin de support de PHP 8.2, voyez notre checklist PHP 8.6 et fin de support de PHP 8.2. Rector réécrit la syntaxe ; il ne détecte pas les changements de comportement à l’exécution, comme les nouveaux défauts de session.
À quoi ressemble le résultat sur un service « ancien », avec withPhpSets() et les prepared sets deadCode et codeQuality — avant :
class InvoiceMailer
{
private $logger;
private $sender;
public function __construct(LoggerInterface $logger, string $sender)
{
$this->logger = $logger;
$this->sender = $sender;
}
public function send(array $invoices, $dryRun = false)
{
$unused = 42;
foreach ($invoices as $invoice) {
if ($dryRun == true) {
continue;
} else {
$this->logger->info(sprintf('Send %s', $invoice->getNumber()));
}
}
return count($invoices) > 0 ? true : false;
}
}Migration Symfony en vue ?
Bear Upgrade migre votre codebase 2,5× à 4,5× moins cher qu'une ESN, selon sa maturité.
Après : promotion de propriétés en readonly, variable inutile supprimée, else retiré, ternaire simplifié. La comparaison $dryRun == true, elle, n’a pas été touchée : la revue de diff et la suite de tests restent les juges.
class InvoiceMailer
{
public function __construct(private readonly LoggerInterface $logger, private readonly string $sender)
{
}
public function send(array $invoices, $dryRun = false)
{
foreach ($invoices as $invoice) {
if ($dryRun == true) {
continue;
}
$this->logger->info(sprintf('Send %s', $invoice->getNumber()));
}
return count($invoices) > 0;
}
}Monter la couverture de types par niveaux
Sur une base existante, activer withPreparedSets(typeDeclarations: true) d’un coup est précisément le big bang à éviter. Rector propose des niveaux : withTypeCoverageLevel(), withDeadCodeLevel(), withPhpLevel(). Chaque incrément ajoute la règle suivante, de la plus sûre à la plus intrusive. Sur notre projet : niveau 10, aucun changement ; niveau 20, des types de retour sur les actions de contrôleur et les ternaires stricts ; niveau 40, les propriétés typées depuis le constructeur. Un niveau peut donc ne rien changer chez vous. Les deux approches sont exclusives — les combiner est refusé : « Your config already enables type declarations set ».
Activez withImportNames() dès le premier jour : sans lui, les règles injectent des noms de classe complets en ligne (\Symfony\Component\HttpFoundation\Response), et vous devrez faire une PR cosmétique pour les nettoyer. Côté verrouillage, Rector écrit les types et PHPStan empêche qu’ils régressent : relevez le niveau de couverture PR après PR, avec une suite verte à chaque palier.
Monter de Symfony 7.4 à 8.x sans big bang
Deux faits d’abord. Symfony 8 exige PHP 8.4.1 au minimum : la montée PHP passe en premier. Et à la mi-octobre 2026, Packagist propose Symfony 8.0 et 8.1 en version stable. La méthode, où chaque étape est une PR courte :
- Rester sur 7.4 LTS, en PHP 8.4 ou plus, et corriger les dépréciations avec la configuration ci-dessus. Observé :
$app->add($cmd)devient$app->addCommand($cmd), etRouting\Annotation\RoutedevientRouting\Attribute\Route. - Convertir les annotations en attributs avec
doctrine: trueetwithAttributesSets(symfony: true). - Typer par paliers : des signatures plus strictes facilitent le passage.
- Lancer
rector composer-basedavant et après le bump pour voir quelles règles s’activent. - Monter :
composer update "symfony/*" --with-all-dependencies, avecextra.symfony.requireà8.1.*. - Relancer Rector, les tests et PHPStan — avec le même
rector.phpqu’à la première étape.
Ce que nous avons mesuré à l’étape 4 tempère les attentes : passer de Symfony 7.4.21 à 8.1.9 a fait passer le compteur de 213 à 215 règles actives sur 370. Le gain automatique du saut de version est donc modeste ; l’essentiel du travail se fait avant, en éliminant les dépréciations de 7.4, et le reste à la main avec les guides UPGRADE. Pour ce tri, suivez notre article sur l’élimination des dépréciations de Symfony 7.4 avant la 8.0. Rector n’est pas une migration Symfony 8 automatique : c’est ce qui rend la partie mécanique gratuite.
Rector en CI : verrouiller les gains
Un job qui échoue dès que Rector réécrirait du code empêche la dette de revenir. validate-config vérifie l’hygiène de la configuration sans traiter de fichier. Attention : la documentation « Cache in CI » montre encore --output-format=github, mais Rector 2.7.0 affiche « The "github" output format is deprecated » — préférez json.
# .github/workflows/rector.yml : échoue si Rector modifierait du code
name: Rector
on:
pull_request:
permissions:
contents: read
jobs:
rector:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: shivammathur/setup-php@34a5396826718e0013f08e3e639d1c315d5f6b23 # 2.35.0
with:
php-version: '8.5'
coverage: none
- run: composer install --no-interaction --prefer-dist
- run: vendor/bin/rector validate-config
- run: vendor/bin/rector process --dry-run --no-progress-bar --output-format=jsonPour découper un gros chantier, deux options de la 2.6.7 sont précieuses : --filter="/Controller/" limite l’exécution à un sous-ensemble de chemins, et --max-changes=N s’arrête après N modifications. Une PR par dossier, une PR par palier.
Les pièges, en une liste
withPhpSets(php86: true)ne fait rien tant quecomposer.jsonannonce>=8.2: mettezrequire.phpà jour d’abord.- Un seul appel à
withPhpSets(), et jamaiswithTypeCoverageLevel()avectypeDeclarations: true. - Plus de
SetList::PHP_*, deSetList::IFni de--output-format=github: dépréciés. - Le résultat de
composer-baseddépend de ce qui est installé : lancez Rector sur la même installation que la CI, idéalement dans le même conteneur. - Rector réécrit, il n’analyse pas : gardez PHPStan en CI pour la détection.
Trois actions pour cette semaine : remplacer vos SetList::PHP_* par withPhpSets() sans argument, ajouter withComposerBased() et lire la sortie de rector composer-based, et planifier la prochaine étape de montée de version en une PR par palier.
Rector automatise la partie mécanique, mais c’est le découpage en paliers et la suite de tests qui rendent une montée de version livrable sans geler la roadmap. Si votre équipe n’a pas le temps de tenir ce chantier en parallèle du produit, c’est exactement le périmètre de Bear Upgrade pour Symfony : Rector, tests et Merge Requests prêtes à merger, palier par palier.

É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