Every new project is different. It depends on the PHP version, framework, libraries, coding style, level of PHPStan without baseline, type coverage or dead code coverage.
Rector runs natively on PHP 7.2 and higher, so you can install it easily. On PHP 7.1, use the following setup.
The most crucial step is to integrate into a new project slowly. We apply Rector rulesets in our clients' projects carefully and knowingly. The goal is not to make a huge change fast but to get you and your teammates comfortable, trust changes in pull requests, and be able to review them.
What does it mean?
Before diving into any prepared sets, we start with the crucial part - the PHP upgrade sets. Let's say this is a composer file of our project:
{
"require": {
"php": "^7.4"
}
}
This means our code base can use features from PHP 7.4, but it doesn't mean it actually uses them. So at first, we check what is the lowest version our codebase uses:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withPhpSets();
This configuration tells Rector, "Upgrade my code to PHP 7.4, based on the composer.json version." Does that sound about right?
No, because this would invoke sets from PHP 5.3, 5.4, 5.5, 5.6, 7.0, 7.1, 7.2, 7.3 and 7.4 simultaneously. That's over 100 rules that will be applied. That sounds dangerous to do and even more tedious to review.
Instead, we take one ruleset at a time:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
- ->withPhpSets();
+ ->withPhpSets(php53: true);
Now, we told Rector, "Instead of applying all PHP sets up to 7.4, apply only sets up to 5.3. "
We run Rector to see the changes it proposes:
vendor/bin/rector --dry-run
If all looks good in the diff, we apply Rector:
vendor/bin/rector
Instead of 100s of rules, we only run a few of them. We can create a small pull request, get a review the same day, and merge.
The next day, we can continue with the following PHP version:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
- ->withPhpSets(php53: true);
+ ->withPhpSets(php54: true);
Again, we run Rector, apply changes, create pull-request, and go for merge. Slowly but surely, we are making our codebase better at a stable and safe pace.
Every codebase is different, and sometimes, we come across a rule that is not safe to apply. We can skip it for now:
<?php
use Rector\Config\RectorConfig;
+use Rector\Php54\Rector\FuncCall\RemoveReferenceFromCallRector;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
+ ->withSkip([RemoveReferenceFromCallRector::class])
->withPhpSets(php54: true);
This is an entirely valid upgrade process. We will deal with skips later once our codebase is upgraded to our PHP version and much more robust.
Check other ways to use withSkip()
.
What is a PHP 8.3 project without a single type declaration?
A horse with Tesla bodywork.
Type coverage is one of the most influential metrics in a modern PHP project. We can have a high PHP version in composer.json
, but our code can still be full of mixed
types, giving us zero confidence. There is a PHPStan package - type-coverage
- that helps raise the bar 1% at a time.
How can we use Rector to help out with type coverage? We can add a prepared set:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
+ ->withPreparedSets(typeDeclarations: true);
Let's run Rector:
vendor/bin/rector --dry-run
Wow, over 90 % of files were changed. That's going to be a very long review. We can do better than that.
Instead of applying ~50 type declaration rules at once, we can apply them individually. This is much easier to review and explain to your team. But which one should we start with?
We took the liberty of sorting the rules from the easy-pick to more complex ones. You can enable them yourself and go one level at a time:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
- ->withPreparedSets(typeDeclaration: true);
+ ->withTypeCoverageLevel(1);
Now run Rector to see the changed files:
vendor/bin/rector
Only five files? We can do that in a day. We create a pull request, get a review, and merge. The next day, we can continue with level 2. You get the idea.
Are you done with the type level and reached 99 % type coverage? It's time to move on to dead code removal.
Again, we could use the prepared dead-code set, but the number of changes would be huge. Instead, we make use of the dead-code level:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
->withTypeCoverageLevel(40)
->withDeadCodeLevel(1);
We increase it by 1, run Rector, create a pull request, get a review, and merge.
Once we reach the highest dead code level, we can move on to next prepared sets.