Final classes bring much more value than extends
it lacks. It teaches composition over inheritance, makes upgrades easier, and even mocking is fine.
If you're lazy like me, you can automate the final
keyword addition to your code - quickly, safely, and check it the CI!
If you wonder "why final" from a technical and architecture point of view, check this amazing writeup by Matthias Noback.
If you care about the real value in the code right now and use Rector and PHPStan, I'll share a little secret with you - final
takes Rector and PHPStan instantly to the next level. How?
We'll look at 3 examples and how these tools see the code:
class GptModel
{
protected function getName()
{
return '5o';
}
public function getContextWindow()
{
return 500_000;
}
}
What are Rector and PHPStan thinking?
"It's a class that may have children"
"There is a protected getName()
method, it seems unused, but maybe the child class uses it"
If we remove this method, we might break child class that depends on it or override it. There is no parent class, so that direction is safe.
"There is public getContextWindow()
method, and it returns a scalar int
value, but maybe child overrides it"
We could add an int
return type declaration, but that would break child classes—simply because they have to be compatible with parent type declarations.
"could, would, but" - the general mood is anxious. We could improve the code, but we're not sure if we won't break something. Better not touch it, right?
"The road to legacy codebase
is paved with good intentions"
This approach is typical for all legacy codebases. Something could be improved and it would instantly bring value, but we're afraid to take the leap. In 5 years, someone else will deal with the mess - it will only take 10x more time and money.
Now, let's see what happens if we add a single keyword:
-class GptModel
+final readonly class GptModel
{
- protected function getName()
- {
- return '5o';
- }
- public function getContextWindow()
+ public function getContextWindow(): int
{
return 500_000;
}
}
What are Rector and PHPStan thinking now?
"Look, this class is what you see is the way you get - we are sure nobody changes it"
"The getName()
method is not used anywhere"
PHPStan spots the getName()
, which is actually private
in the context of final
classes. Rector will turn it into private
... and then, they can see it's never used. Rector will remove it safely.
"The getContextWindow()
method returns int
"
This method is never changed by the child class, so we can add type declaration int
safely. Rector will do that for us.
As a bonus, we see that values are never changed - this class is readonly
.
final
keyword bring to your project?protected
are turned into private
private
methods are removedreadonly
protected
/private
called methodsfinal
Adding final
doesn't have to be pain and BC breaks all over the codebase.
We at Rector made a simple tool that adds final
to classes:
Try an experiment to see for yourself:
Include PHPStan fixes and Rector changes and you're done - pure value.
You can revert step 1 to keep the codebase as it was.
Final
by default to unlock productivityWe add final
to all classes in private projects and the value only compounds.
Developer teams get more self-confident, more productive, and more relaxed. It's safe to make huge refactoring and last but not least, Rector and PHPStan have our back.
Happy coding!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!