There are 2 ways to use PHPStan. You can use native levels, and official extensions and raise the level from 0 to 8. This is a good start, but it often requires enormous work and brings must-have value.
There is also a 2nd way: I wanted PHPStan to be more fun and more tailored to the unique projects I work with. That's why I made symplify/phpstan-rules, a package that just crossed 6 200 000 downloads. It is one of the most used PHPStan extensions apart from official ones.
I put all the fun and practical rules there, and often they prove to be useful to others too.
But today I want you to move from end-user to creator.
"You teach me, I forget. You show me, I remember.
You involve me, I understand."
Today we write a custom PHPStan rule together. Not for everyone, but only for you and your local project. We will not write tests, we will not make it 100 % reliable, and we will not cover all edge cases. We will just make it bring value, and make it fun and practical.
That's the real beauty of my own local PHPStan rules - they can be KISS β Simple & Stupid. I don't have to feel ashamed on socials if they seem too vague or for everyone.
I work on various codebases, raising type coverage one 1 % at a time. It's a lot of manual work or Copilot exchanges I need to verify. In short: repetitive thinking hurts my brain.
Last week, I noticed a simple pattern in one of the codebases:
public function get($userId): User
{ /* ... */ }
public function run($userId): void
{ /* ... */ }
public function request($userId, array $params): void
{ /* ... */ }
There is a type missing for $userId
... what if we know it's always int
or string
?
I've checked other calls in codebase + database and made an astounding discovery: the user id is always an int
!
I wondered: what if we make a PHPStan rule that:
I often have no idea if the PHPStan rule will work or not, so I put 10 10-minute experiment limit on it. If it doesn't work, I just throw it away. If it does, we keep the rule and improve it.
Let's dive in!
First, make a directory:
/utils/phpstan/src
There, we create an empty ParamTypeByNameRule.php
class:
/utils/phpstan/src/ParamTypeByNameRule.php
composer.json
{
"autoload-dev": {
"psr-4": {
"Utils\\PHPStan\\": "utils/phpstan/src",
}
}
}
Refresh PSR-4 paths:
composer dump-autoload
The boring setup is done, let's write the fun part! What should our PHPStan rule do?
userId
? suggest an int
Easy-peasy! I think it will be the shortest custom rule ever written.
<?php
namespace Utils\PHPStan;
use PHPStan\Rules\Rule;
use PhpParser\Node\Param;
class ParamTypeByNameRule implements Rule
{
public function getNodeType(): string
{
return Param::class;
}
/**
* @param Param $node
*/
public function processNode(Node $node, Scope $scope): array
{
// we know the type - let's skip it
if ($node->type !== null) {
return [];
}
// what is the parameter name?
$parameterName = $node->var->name->toString();
if ($parameterName !== 'userId') {
return [];
}
return [
RuleErrorBuilder::message('The $userId param is missing `int` type')
->identifier('custom.paramTypeByName')
->build();
];
}
}
That's all folks!
phpstan.neon
rules:
- Utils\PHPStan\ParamTypeByNameRule
Protip: try running only this rule alone without any levels:
parameters:
customRulesetUsed: true
# comment out level
# level: 8
vendor/bin/phpstan
That's it!
Does it bring value? If yes, improve it. If not, throw it away.
The next step would be to add more param-nameβ pairs - like $articleId
, $groupName
etc.
This rule was so much fun to write and use, that I've turned it into a generic one. Raising param type coverage is one the most complex type coverages, and this rule turned it into a fun game that saves time and brain power:
This is one of the most weird, laziest and easiest PHPStan rules I've ever written... π
— Tomas Votruba (@VotrubaT) March 12, 2025
...and the beauty is, it brings instant real value
to any codebase with missing type declarations π pic.twitter.com/4XQTo8ebhV
Now it's your turn to make however weird, stupid, simple, non-sense PHPStan rule you want. Don't forget - nobody's watching and you can be creative beyond reason.
Can you think it? Write it!
It's your project, your rules, your fun, and if it brings any value, stick with it.
Writing custom PHPStan rules is one of the greatest assets when it comes to raising codebase value in time. It stays in the project after you leave and helps others to keep the codebase clean and safe.
Happy coding!