Why are final
classes so valuable for automated tools? Let's see this code:
class Conference
{
protected $name;
public function __construct(string $name)
{
$this->name = $name;
}
// ...
}
Static analysis and Rector could do so much here... but they're not sure:
$name
property?Let's see what Rector can do if we add a final
keyword:
-final class Conference
+final readonly class Conference
{
- protected $name;
-
- public function __construct(string $name)
+ public function __construct(private string $name)
- {
- $this->name = $name;
- }
// ...
}
We've just shifted from PHP 7.0-like code to PHP 8.2-like.
In the case of a massive project with many classes, we would not get much work done manually. That's why we need automation to handle this for us.
Rector to the rescue? There was once a rule in Rector called FinalizeClassesWithoutChildrenRector
. It was pretty helpful, but it also did many false positive changes, so I deprecated it.
What now?
We removed a buggy Rector rule, but that doesn't help new projects implement final
fast and safely. What is the minimal goal we want to achieve?
We need a tool that can:
class SomeClass extends ParentClassThatCannotBeFinal
{
}
Defined in attribute:
use Doctrine\ORM\Mapping\Entity;
#[Entity]
class ThisIsEntity
{
}
...docblock:
use Doctrine\ORM\Mapping\Entity;
/**
* @Entity
*/
class ThisIsAlsoEntity
{
}
...but also in YAML mapping configs:
class ThisIsAlsoEntityWithMappingInYAML
{
}
Those are extended by mocking framework, so they have to be skipped.
namespace PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$someMock = $this->createMock(SomeRepository::class);
}
}
This is optional, as we separate tests and source code.
Imagine having this amazing tool with all the features mentioned above, but when you run composer require,
it will conflict with Symfony 7 vs. Symfony 5 in your project.
Last but not least, we want to make the tool available the most PHP devs. We have to:
What needs to be done? When we check the first principles, it seems pretty straightforward:
final
to the restNote: If you're new to AST, check out this super fun and practical talk by Marcel Pociot about Parsing PHP for fun and profit.
The first prototype took about 3 days to build. Then, it took 2 more months of internal testing on real projects to improve with feedback. Today, I'm proud to share it with the public.
We use this technique in every project we help upgrade, so we called it accordingly - a swiss knife.
composer require rector/swiss-knife --dev
vendor/bin/swiss-knife finalize-classes app tests --dry-run
Use --dry-run
on the first run to be safe.
Does all seem reasonable to you? Let's roll:
vendor/bin/swiss-knife finalize-classes app tests
It is better to separate tests leaking requirements to your source code and use bypass final. But maybe you don't want to deal with it right now.
That's why we have the --skip-mocked
option that keeps all mocked classes without final
:
vendor/bin/swiss-knife finalize-classes app tests --skip-mocked
That's it!
Protip: add the --dry-run
to your CI to spot these classes early and delegate work to your CI.
Is there a spot that was finalized incorrectly? Create an issue in the Github; we'll cover it.
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!