As you know, we provide an upgrade services to speed up the modernization of codebases. Part of this service is getting PHPStan to level 8 with no baseline (only edge cases).
Level 6 is known for requesting more detailed types over array
,
iterable
or Iterator
type hints. Bare mixed
or array
should be replaced with explicit key/value types, e.g., string[]
or array<int, SomeObject>
.
At first, we did this work manually. Later, we made custom Rector rules that we kept private.
Today, we are open-sourcing these rules to help you with the same task.
When you're proud that your project passes PHPStan on level 7...
— Tomas Votruba (@VotrubaT) September 30, 2025
...and then remove that single ignore line ↓ pic.twitter.com/VNKYWEEZoa
1573 errors... Rector should be able to help with that, right?
Let's look at a few examples that are missing detailed types and that Rector can improve now:
function getNames(): array
{
return ['John', 'Jane'];
}
Now this is straightforward; it can be improved to:
+/**
+ * @return string[]
+ */
function getNames(): array
{
// ...
}
Why do this manually in 100s of places, if Rector can do it for you?
Let's look at another example:
function getUsers(): array
{
$user = [];
$users[] = new User('John');
$users[] = new User('Jane');
return $users;
}
No-brainer:
+/**
+ * @return User[]
+ */
function getUsers(): array
{
// ...
}
What if there are multiple different objects that all share a single contract interface?
final class ExtensionProvider
{
public function provide(): array
{
return [
new FirstExtension(),
new SecondExtension(),
];
}
}
In a real project, we would have to open all of those classes, check parent classes and interfaces, and try to find the first common one. Now we don't have to, Rector does it for us:
+ /**
+ * @return ExtensionInterface[]
+ */
public function provide(): array
{
// ...
}
array_map()
returnWe can infer the type from functions like array_map()
:
+/**
+ * @return string[]
+ */
public function getNames(array $users): array
{
return array_map(fn (User $user): string => $user->getName(), $users);
}
What if the method is private and is called only in a local class? We can now collect all the method calls and learn their type:
final class IncomeCalculator
{
public function addCompanyTips(): void
{
$this->addTips([100, 200, 300]);
}
public function addPersonalTips(): void
{
$this->addTips([50, 150]);
}
+ /**
+ * @param int[] $tips
+ */
private function addTips(array $tips): void
{
}
}
...and many more. Right now, the initial set contains 15 rules, and we plan to extend it further. Got an idea for an obvious rule that you keep doing manually and is not covered yet? Let us know.
We designed these rules to avoid filling useless types like mixed
, mixed[]
, or array
. If the Rector doesn't know better, it will skip these cases. We want to fill those types they way humans would do to improve code readability and static analysis.
Rector is smart enough to keep detailed types, but override those dummy ones:
/**
- * @return mixed[]
+ * @return string[]
*/
function getNames(): array
{
return ['one', 'two']
}
The best way to start using this set is via level feature. Add this single line to your rector.php
config:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withTypeCoverageDocblockLevel(0);
And take it one level at a time:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
- ->withTypeCoverageDocblockLevel(0);
+ ->withTypeCoverageDocblockLevel(1);
In a rush or feeling lucky? Add full set:
<?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPreparedSets(typeDeclarationDocblocks: true);
We've put a lot of work into making rules balanced and reliable, but it's still in the early testing phase. Give it a go, let us know how slim your baseline got after a single Rector run.
Happy coding!