Introduction: Errors Belong to Developers, Not Users
#blogPostAsWebApp: https://voku.github.io/WhyPHPStan/
Let’s start with something simple and uncomfortable:
If an error happens at runtime, a user sees it.
I don’t care how good your monitoring is, how fast your rollback pipeline runs, or how often you say “it shouldn’t happen”. Runtime errors are already too late. They are failures that escaped development and reached production.
That’s the entire motivation for static analysis.
Not theory. Not fashion. Damage control.
PHP enforces reality.
Static analysis enforces possibility.
Two different layers. Two different failure modes.
And if we can use both, the real question is: why wouldn’t we?
PHP Enforces Reality. Static Analysis Enforces Possibility.
PHP is excellent at telling you when something actually breaks:
- calling a method that doesn’t exist
- passing a string where an int is required
- accessing a property on
null
That’s runtime reality.
But PHP is deliberately permissive before that point. It allows code to exist even if it can only fail under certain conditions, in certain branches, or after a refactor.
That’s where static analysis comes in.
Tools like PHPStan don’t compete with PHP. They operate earlier:
- when code is written
- when it’s refactored
- when it’s reviewed
- when it’s understood by someone new
PHP tells you what happened.
Static analysis tells you what should never have been possible.
Different jobs. Same goal.
High PHPStan Levels Don’t Just Find Bugs — They Shape Architecture
This is where many people misunderstand strict static analysis.
A high PHPStan level is not about “more errors”.
It’s an architectural pressure system.
It pushes you to:
- stop passing anonymous arrays around
- stop encoding meaning in strings
- stop hiding invariants in comments
- introduce value objects, enums, interfaces
- define clear boundaries and contracts
And yes, there’s a very pleasant side effect:
👉 IDE autocompletion becomes frighteningly good
Because once types are precise, the IDE actually understands your system.
Not guesses. Not mixed. Not “trust me”.
That’s not cosmetic DX. That’s speed and confidence.
“Let PHP Enforce Contracts” — Good. Now Add the Missing Layer.
“Let PHP enforce contracts.”
Perfect. We should.
But nothing stops you from:
- enforcing contracts at runtime and
- enforcing them at design time
In fact, PHPStan often nudges you toward better PHP:
- stricter parameter types
- explicit return types
- real interfaces instead of conventions
- enums instead of magic strings
Using static analysis doesn’t weaken PHP’s guarantees.
It narrows the space in which PHP ever has to complain.
DX Matters: Instant Feedback Beats Any Test Suite
One of the most underrated benefits is developer experience.
If a function declares:
/**
* @param int<1, 32> $limit
*/
function paginate(int $limit): void {}
And I pass 64, I want that error:
- immediately
- in my IDE
- without running tests
- without pushing code
- without involving CI
That feedback loop is priceless.
Static analysis is the only tool that gives you semantic errors at typing speed.
Security: literal-string Is a Real Guardrail
This is not theoretical.
/**
* @param literal-string $sql
*/
function runRaw(string $sql): void {}
This single constraint:
- blocks user input
- blocks “temporary” hacks
- blocks accidental string concatenation
- blocks whole classes of SQL injection mistakes
Does it solve all security? Of course not.
Does it eliminate dumb, expensive errors early? Absolutely.
Documentation That Cannot Lie
Traditional documentation rots because nothing enforces it.
Static PHPDoc does.
When you write:
/**
* @return Generator<int, User>
*/
function users(): Generator {}
You’ve documented:
- key type
- value type
- iteration semantics
And if reality diverges? The build fails.
As a bonus:
- the IDE knows
$keyisint - the IDE knows
$userisUser - refactors are safe
- onboarding is faster
This is documentation with teeth.
Logic Bugs Still Exist 😊 — And That’s Fine
Let’s get this out of the way:
Static analysis does not prevent logic bugs.
Nothing does — except thinking.
But static analysis removes:
- type bugs
- shape bugs
- nullability bugs
- refactor bugs
- “this can never happen” bugs
Those are not interesting problems.
They’re noise.
I prefer to spend my brainpower on logic and design, not on mistakes a machine can detect instantly.
The Practical Rule Set That Actually Works
This is how static analysis succeeds in real PHP codebases:
- keep arrays and strings at the boundaries (HTTP, JSON, DB)
- validate once
- convert once
- after that: objects, enums, value objects
- baseline legacy code
- tighten rules only where you touch code
- never weaken types just to silence the tool
No rewrites. No greenfield fantasies.
Just steady entropy reduction.
Conclusion: Two Layers Are Better Than One
PHP enforces reality.
Static analysis enforces possibility.
Runtime errors mean errors for users.
Static errors mean errors for developers.
High PHPStan levels shape architecture for the better and massively improve IDE feedback.
Nothing prevents you from using strong PHP types and static analysis — in fact, they reinforce each other.
Logic bugs still require thinking 😊
But type bugs are optional.
And I prefer optional bugs to be… not present.
Summary
- Runtime errors are user-facing failures
- Static analysis shifts errors to development time
- High PHPStan levels improve architecture and DX
- Security and documentation benefit immediately
- PHP and PHPStan are complementary, not competing
- Logic bugs remain — but the boring ones disappear
What’s Next?
- Using PHPStan as an architectural guardrail
- Static analysis as a safety net for LLM-generated code (with a fast feedback loop)
- Killing
mixedonce and for all
Happy Coding.
Top comments (0)