eXtreme Legacy 7: Rector 26/11/2025
π€ Behold Rector, the Metamorphosis Overlord, wielding the dark grimoire of AST manipulation to transmute ancient incantations of PHP 5.6 into the blazing runes of PHP 8.x through unholy automated rituals! π₯
In this series, we're dealing with BisouLand, an eXtreme Legacy application (2005 LAMP spaghetti code base). So far, we have:
- π got it to run in a local container
- π¨ written Smoke Tests
- π― written End to End Tests
- π§Ή created and applied Coding Standards
- β migrated to PDO
- π upgraded PHP 5 to PHP 8
This means we can run it locally (http://localhost:43000/), and have some level of automated tests.
But even with a modern PHP version, we still have an eXtreme Legacy spaghetti code...
So let's use Rector to apply some automated refactorings
- What is the difference with PHP CS Fixer
- What is Rector
- Early Return
- Code Quality
- Type Declaration
- Dead Code
- Cherry Pick your rules
- Conclusion
What is the difference with PHP CS Fixer
A question I often get asked is: what's the difference between rector and PHP CS Fixer?
PHP CS Fixer was initially created to automatically fix coding style issues, such as "indentation: 4 spaces vs 1 tab", or "opening curly brace: one the same line vs on a new line", etc.
With time it grew to do more than just fixing coding style issues, like replacing deprecated function calls to their modern counterparts.
But it is a token-based parsing tool (using PHP's token_get_all()),
meaning it looks at the code as a series of keywords (class), whitespaces (),
identifiers (MyClass), punctuations ({), etc,
it's very much a flat array.
This means that there's a limit to how much it can know and understand about the code, and therefore how (and when) to modify it.
For all intent and purposes we should treat PHP CS Fixer as a Coding Style enforcer, even if it can do more than that.
What is Rector
Rector was created to automatically refactor code, such as adding type declarations, simplifying complex control flow, or removing dead code.
With time it grew to do more than just refactoring code, like fixing coding style.
So there is some overlap with PHP CS Fixer,
but Rector is an Abstract Syntax Tree (AST) tool (using PHP Parser),
meaning it holds an structured model of the code (Class object,
with identifier attribute set to MyClass,
collection of Property objects each having a visibility, type and identifier attributes, etc).
This means it can have a deep understanding of the code and therefore how
to modify it in a way that is safe (for example it won't add a string typehint
to a function argument if somewhere in the code a bool is passed to it
or if it's used as an integer inside the function).
Some people know it as the "one off automatic upgrade tool", because it can:
- upgrade your PHP app to newer versions (as we did in the previous article)
- upgrade your PHPUnit testsuite to newer versions (e.g. replace PHPdoc
@testannotation with#[Test]attributes) - upgrade your Symfony app to newer version
But is is more useful as a "linting" tool that is run on every change made (for example in the CI): once all the deprecated function calls have been replaced with their modern counterparts, you don't want someone reintroducing a deprecated function call by accident.
But coding style and upgrades are secondary features of Rector, its main one is to allow you to define Code Quality rules that are going to be automatically enforced as part of your CI workflow.
Out of the box, rector provides you with Rules as well as rule Sets, let's have a look at what they can do for us.
Early Return
The Early Return set is a collection of rules that will check if your code can be simplified with earlier return:
- PreparedValueToEarlyReturnRector
- ReturnBinaryOrToEarlyReturnRector
- ReturnEarlyIfVariableRector
- ChangeNestedIfsToEarlyReturnRector
- RemoveAlwaysElseRector
- ChangeIfElseValueAssignToEarlyReturnRector
- ChangeOrIfContinueToMultiContinueRector
- ChangeNestedForeachIfsToEarlyContinueRector
Let's enable it in the rector.php config file:
<?php
declare(strict_types=1);
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;
return RectorConfig::configure()
->withCache(
cacheDirectory: '/tmp/rector',
cacheClass: FileCacheStorage::class,
)
->withPaths([
__DIR__,
__DIR__.'/../monolith',
])
->withSkip([
// ββ Excluded paths βββββββββββββββββββββββββββββββββββββββββββββββββββ
// [qa]
__DIR__.'/vendor',
// [monolith]
__DIR__.'/../monolith/vendor',
])
->withSets([
// ββ PHP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::PHP_84,
// ββ Core βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::EARLY_RETURN,
])
->withRules([
]);
Now let's run it using:
make rector # ./vendor/bin/rector process
In the BisouLand codebase, this applies the ReturnEarlyIfVariableRector rule:
function calculterAmour($CalAmour, $timeDiff, $LvlCoeur, $nb1, $nb2, $nb3)
{
$CalAmour = calculerGenAmour($CalAmour, $timeDiff, $LvlCoeur, $nb1, $nb2, $nb3);
// Cette fonction ajoute un frein sur le minima.
if ($CalAmour < 0) {
- $CalAmour = 0;
+ return 0;
}
return $CalAmour;
}
As well as the RemoveAlwaysElseRector rule:
// Permet de convertir un timestamp en chaine sous la forme heure:minutes:secondes.
function strTemps($s): string
{
$m = 0;
$h = 0;
if ($s < 0) {
return '0:00:00';
- } else {
- if ($s > 59) {
- $m = floor($s / 60);
- $s = $s - $m * 60;
- }
- if ($m > 59) {
- $h = floor($m / 60);
- $m = $m - $h * 60;
- }
- $ts = $s;
- $tm = $m;
- if ($s < 10) {
- $ts = '0'.$s;
- }
- if ($m < 10) {
- $tm = '0'.$m;
- }
- if ($h > 24) {
- $d = floor($h / 24);
- $h = $h - $d * 24;
- $h = $d.' jours '.$h;
- }
-
- return $h.' h '.$tm.' min '.$ts.' sec';
+ }
+
+ if ($s > 59) {
+ $m = floor($s / 60);
+ $s = $s - $m * 60;
+ }
+
+ if ($m > 59) {
+ $h = floor($m / 60);
+ $m = $m - $h * 60;
+ }
+
+ $ts = $s;
+ $tm = $m;
+ if ($s < 10) {
+ $ts = '0'.$s;
+ }
+
+ if ($m < 10) {
+ $tm = '0'.$m;
+ }
+
+ if ($h > 24) {
+ $d = floor($h / 24);
+ $h = $h - $d * 24;
+ $h = $d.' jours '.$h;
+ }
+
+ return $h.' h '.$tm.' min '.$ts.' sec';
}
Code Quality
The Code Quality rule set includes a whopping 78 rules. I've enabled it as follow:
// Extract from file: rector.php
// [...]
// ββ Core βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::CODE_QUALITY,
SetList::EARLY_RETURN,
And after running it on the BisouLand codebase, out of the 78 the following 8 have been applied:
- UseIdenticalOverEqualWithSameTypeRector
- SimplifyBoolIdenticalTrueRector
- CombineIfRector
- ShortenElseIfRector
- SimplifyIfElseToTernaryRector
- AbsolutizeRequireAndIncludePathRector
- SwitchNegatedTernaryRector
- CombineIfRector
Now let's focus on UseIdenticalOverEqualWithSameTypeRector,
"Use === / !== over == / !=, if values have the same type".
There is similar rule in PHP CS Fixer, strict_comparison, but if we configure that it'll change ALL comparisons in the codebase to strict, whether it's safe to do so or not.
Rector is able to detect the type of variables, so it'll only apply it if it's safe to do so,
for example in nuage.php:
$sautPossible = 0;
// Au moins saut niveau 1.
if ($nbE[2][3] > 0) {
$distance = abs(16 * ($nuageL - $nuageSource) + $i - $positionSource);
// On prend en compte les jambes, et le niveau de saut.
$distMax2 = distanceMax($nbE[0][4], $nbE[2][3]);
if ($distance <= $distMax2) {
$sautPossible = 1;
}
}
- if (1 == $sautPossible) {
+ if (1 === $sautPossible) {
$sautPossible was defined with an integer (should really be a boolean, but whatever),
is potentially (in a if) set to another integer value, so it's safe to strictly
compare it to an integer.
Type Declaration
In addition to value assignments ($sautPossible = 0), Rector can know the type
of a variable from the Type Hints of functions they are passed in or that return them.
In eXtreme Legacy applications like BisouLand, we do not have such type Hints, but Rector can add them for us (again only if it knows it's safe to do so), thanks to the 63 rules in the set Type Declaration.
For example NumericReturnTypeFromStrictScalarReturnsRector:
// Fonction qui retourne 0 si joueurAutre est meme niveau, 1 s'il est intouchable parce que trop faible, 2 s'il est intouchable parce que trop fort.
- function voirNiveau($scoreJoueur, $scoreAutre)
+ function voirNiveau($scoreJoueur, $scoreAutre): int
{
if ($scoreJoueur < 50) {
return 2;
}
if ($scoreAutre < 50) {
return 1;
}
if ($scoreJoueur > 2000 && $scoreAutre > 2000) {
return 0;
}
if (abs($scoreAutre - $scoreJoueur) <= 200) {
return 0;
}
if ($scoreJoueur - $scoreAutre > 200) {
return 1;
}
return 2;
}
The advantage of keeping Rector as part of your CI means that as you improve the codebase, some previous rules will be able to be automatically applied again, such as UseIdenticalOverEqualWithSameTypeRector!
Previously in nuage.php, $Niveau's type was considered as mixed, but thanks to the return type hint
Rector now knows it's an integer, and so that it's safe to use strict type comparison:
$Niveau = voirNiveau($scoreSource, $score);
- if (1 == $Niveau) {
+ if (1 === $Niveau) {
if ($score >= 50) {
echo '<a class="bulle" style="cursor: default;color:blue;" onclick="return false;" href=""><strong>',$donnees_info['pseudo'],'</strong><span style="color:blue;">Joueur trop faible</span>';
} else {
echo '<a class="bulle" style="cursor: default;color:teal;" onclick="return false;" href=""><strong>',$donnees_info['pseudo'],'</strong><span style="color:teal;">Joueur ayant moins de 50 points</span>';
}
- } elseif (0 == $Niveau) {
+ } elseif (0 === $Niveau) {
echo '<a class="bulle" style="cursor: default;color:red;" onclick="return false;" href=""><strong>',$donnees_info['pseudo'],'</strong><span style="color:red;">Ce joueur a ton niveau</span>';
Dead Code
Another powerful rule set is Dead Code and its 55 rules.
For example in RemoveAlwaysTrueIfConditionRector,
Rector is able to detect conditions that would always evaluate to true,
and therefore remove the condition check altogether.
In the following snippet, Rector detects that $resultat is always set
because every single code path assigns it a value,
making the isset($resultat) check redundant:
if (isset($_GET['Dnuage'], $_GET['Dpos']) && !empty($_GET['Dnuage']) && !empty($_GET['Dpos'])) {
$Dnuage = htmlentities((string) $_GET['Dnuage']);
$Dpos = htmlentities((string) $_GET['Dpos']);
if ($nbE[0][5] > 0) {
$stmt = $pdo->prepare('SELECT id, oeil, score, pseudo FROM membres WHERE nuage = :nuage AND position = :position');
$stmt->execute(['nuage' => $Dnuage, 'position' => $Dpos]);
if ($donnees = $stmt->fetch()) {
// [...]
if (0 == $Niveau) {
if ($amour >= $cout) {
$resultat = "Tu as dΓ©visagΓ© {$pseudoCible}";
// [...]
} else {
$resultat = "Tu n'as pas assez de Points d'Amour";
}
} else {
$resultat = "Tu n'as pas le mΓͺme niveau que ce joueur";
}
} else {
$resultat = "Il n'y a plus de joueur a cette position";
}
} else {
$resultat = 'Il te faut des yeux niveau 1 pour dΓ©visager un joueur';
}
?>
<h1>DΓ©visager</h1>
<br />
<a href="<?php echo $Dnuage; ?>.nuage.html">Retourner sur le nuage en cours</a><br />
<br />
<?php
- if (isset($resultat)) {
- echo '<span class="info">[ '.$resultat.' ]</span><br /><br />';
- }
+ echo '<span class="info">[ '.$resultat.' ]</span><br /><br />';
Cherry Pick your rules
There are more rule sets, here are the ones that have been configured for BisouLand:
<?php
declare(strict_types=1);
use Rector\Caching\ValueObject\Storage\FileCacheStorage;
use Rector\CodingStyle\Rector\Closure\StaticClosureRector;
use Rector\CodingStyle\Rector\FuncCall\ArraySpreadInsteadOfArrayMergeRector;
use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclarationDocblocks\Rector\Class_\AddReturnDocblockDataProviderRector;
use Rector\TypeDeclarationDocblocks\Rector\Class_\ClassMethodArrayDocblockParamFromLocalCallsRector;
use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromGetterReturnRector;
use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarArrayFromPropertyDefaultsRector;
use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarFromParamDocblockInConstructorRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockBasedOnArrayMapRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromAssignsParamToParamReferenceRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDataProviderRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDimFetchAccessRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForArrayDimAssignedObjectRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForCommonObjectDenominatorRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockGetterReturnArrayFromPropertyDocblockVarRector;
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockReturnArrayFromDirectArrayInstanceRector;
use Rector\Visibility\Rector\ClassConst\ChangeConstantVisibilityRector;
use Rector\Visibility\Rector\ClassMethod\ChangeMethodVisibilityRector;
return RectorConfig::configure()
->withCache(
cacheDirectory: '/tmp/rector',
cacheClass: FileCacheStorage::class,
)
->withPaths([
__DIR__,
__DIR__.'/../monolith',
])
->withSkip([
// ββ Excluded paths βββββββββββββββββββββββββββββββββββββββββββββββββββ
// Excluded folders
// [qa]
__DIR__.'/vendor',
// [monolith]
__DIR__.'/../monolith/vendor',
// ββ Excluded rules βββββββββββββββββββββββββββββββββββββββββββββββββββ
// [CODE_QUALITY]
Rector\CodeQuality\Rector\Assign\CombinedAssignRector::class,
// [CODING_STYLE]
Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector::class,
])
->withSets([
// ββ PHP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::PHP_84,
// ββ Core βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SetList::CODE_QUALITY,
SetList::CODING_STYLE,
SetList::DEAD_CODE,
SetList::EARLY_RETURN,
SetList::INSTANCEOF,
SetList::NAMING,
SetList::PRIVATIZATION,
SetList::STRICT_BOOLEANS,
SetList::TYPE_DECLARATION,
// ββ PHPUnit ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::PHPUNIT_120,
])
->withRules([
// ββ Core βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// PHPdoc array types
AddParamArrayDocblockBasedOnArrayMapRector::class,
AddParamArrayDocblockFromAssignsParamToParamReferenceRector::class,
AddParamArrayDocblockFromDataProviderRector::class,
AddParamArrayDocblockFromDimFetchAccessRector::class,
AddReturnDocblockDataProviderRector::class,
AddReturnDocblockForArrayDimAssignedObjectRector::class,
AddReturnDocblockForCommonObjectDenominatorRector::class,
AddReturnDocblockForJsonArrayRector::class,
ClassMethodArrayDocblockParamFromLocalCallsRector::class,
DocblockGetterReturnArrayFromPropertyDocblockVarRector::class,
DocblockReturnArrayFromDirectArrayInstanceRector::class,
DocblockVarArrayFromGetterReturnRector::class,
DocblockVarArrayFromPropertyDefaultsRector::class,
DocblockVarFromParamDocblockInConstructorRector::class,
// Inherit parent visibility
ChangeConstantVisibilityRector::class,
ChangeMethodVisibilityRector::class,
// More Coding Style
ArraySpreadInsteadOfArrayMergeRector::class,
StaticClosureRector::class,
]);
Not all rule sets have been added, for example we are missing:
There are also some rules from added rulesets that we have decided to exclude:
- CombinedAssignRector
Simplify
$value = $value + 5;assignments to$value += 5; - EncapsedStringsToSprintfRector
Convert
"{$value}"tosprintf('%s', $value)or''.$value
Finally, there are some rules that are not part of any rule set, which we have added one by one.
My advice to select only the rule set and rules you like, by first trying them all.
You can run Rector in "check only" mode for that purpose:
./vendor/bin/rector process --dry-run
Conclusion
Rector is a powerful tool that goes way beyond simple one-off upgrades.
By integrating it into your CI workflow, you can enforce Code Quality rules that continuously improve your codebase as it evolves.
The key difference with PHP CS Fixer is that Rector understands your code's structure and types through AST parsing, which means it can make safe refactoring decisions that a token-based tool simply cannot.
For BisouLand, we've seen Rector automatically:
- simplify control flow with early returns
- enforce strict type comparisons where safe
- add type declarations to functions
- remove dead code and unnecessary conditions
And as the codebase improves (more type hints, better structure), Rector becomes even more effective at applying additional rules automatically.
My recommendation is to start with the rule sets that make sense for your project,
run them in --dry-run mode to review the changes,
and gradually build up your configuration by cherry-picking the rules you like.
Then keep Rector running in your CI to prevent regressions and maintain the code quality improvements you've worked so hard to achieve.
Now with our almost not spaghetti code, we truly are in the future.
βοΈ What do you mean, "still using old MySQL database"?