Rector in legacy projects
After collecting some experience with introducing Rector to legacy projects, I want to write down what I have learned along the way.
Goal
The article describes how to utilize Rector to maximize type coverage of a legacy project. The more types are defined in the codebase the better the results of your IDE or static analysis tools will be.
This is usually the first thing you should do, before applying more advanced rector code transformations. Rector can be used in a similar way to apply other Rules or Rulesets.
Additionally more type coverage is a great first step after a PHPStan/Psalm setup, to make sure static analysis can find relevant bugs efficiently. Otherwise adding types to a old codebase can take a lot of time. Doing it manually is also prone to errors.
Overall plan
These are the top level steps I try to follow:
- Setup
- make sure you have PHPStan configured for your project, at least at level 5.
- add TomasVotruba/type-coverage to create type coverage information
- create a PHPStan baseline with all existing errors
- analyze your baseline, to get an idea of the overall type coverage of the project
- Preparation
- Fix all āImplicit array creation is not allowed - variable ā¦ does not existā PHPStan errors
- Fix all āVariable ā¦ might not be definedā PHPStan errors
- Adding Types - order is important!
- Add return types
- Add property types
- Add parameter types
- re-generate and re-analyze your baseline to see the improvements / you might create a PHPStan baseline trend report
Analysing the baseline is technically not required. Crunching the numbers can help keep a dev team motivated or these can be used to convince management people about your current state and potential goals.
Setup
The preparation steps and the linked articles in the āoverall planā-chapter should contain all you need.
Preparation
Fixing the mentioned PHPStan errors to make sure Rector can trust your variables.
Adding Types with Rector
Start with Rector as described in the introduction. Make sure you have all relevant source paths configured and the setup works as expected.
We will run Rector in the command line on your workstation. Later on you may configure Rector as part of your CI pipeline, but thatās a topic for another article.
Working with Rector usually means you start by adding one Rector rule at a time. Let the tool do its magic and review the generated changes. Make sure you feel confident with them. If you get overwhelmed by the amount of changes, revert the working state and run your current Rector rule only against a few paths instead of the whole project.
Repeat using smaller steps as long as you feel the result is not reviewable. How often you need to divide the steps into smaller ones depends on the rule being applied and your codebase.
Between these steps you should commit the intermediate states. This also eases seeing the actual differences between the steps.
NOTE:
Especially in legacy projects its important to make sure rector is not relying on PHPDoc types. This is what *Strict*
rector rules are for. If you apply non-Strict rector rules, take special care your PHPDoc is precise.
Add return types
Itās important to add return types first, as itās the least risky change and should be backwards compatible most of the time.
- If your codebase is pretty large, you may start with
final
classes first. - As long as you donāt add new return types to methods which gets overridden in a subclass, you should be fine.
- Give classes some extra attention which somehow integrate with libraries you use, like e.g. Doctrine-Collections.
- If classes implement magic methods (e.g.
__get
), review related changes properly.
If rector changes things you donāt like, you may ignore source files for single rules or even skip the source file completely. You can re-visit the skipped cases later again. You may feel more confident after the codebase got enriched with types and PHPStan can better understand the code in question.
I had the most success using the ReturnTypeFromStrict*
Rector rules first.
Do so one rule at a time, like described above.
Add property types
In the next step in my experience itās best to add property types.
Start with private
properties and later move on to protected
ones of final
classes.
If you are not sure about nullability, keep using nullable types for now.
Last add types to protected
properties of non-final classes and public
properties.
Keep in mind that adding types to public/protected properties to classes which use inheritance can be BC break.
I had the most success using the PropertyTypeFromStrict*
Rector rules first.
After that try the TypedPropertyFrom*
rules.
Add parameter types
Last but not least add parameter types. Be careful, as adding parameter usually breaks backwards compatibility. Thatās especially important in case you work on library code, as it might force you to create a new major version.
I had the most success using the *ParamType*
Rector rules.
Give back
In case you find this content useful, please consider supporting my open source work.
Found a bug? Please help improve this article.