Static Analysis with PHPStan and Psalm
By the end of this lesson you'll catch bugs without running your code — install and run PHPStan and Psalm , understand strictness levels and type inference, write precise @param / @return / @var PHPDoc (including generics), use a baseline to adopt analysis on legacy code, configure phpstan.neon / psalm.xml , and wire it all into CI.
Learn Static Analysis with PHPStan and Psalm in our free PHP course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Installing & Running the Tools
PHPStan and Psalm are the two standard static analyzers for PHP. You install them as dev-only dependencies (they're tooling, not part of your shipped app), then point them at your source folder. They read and reason about your code and print any problems — crucially, without ever running it. That's why they catch bugs on code paths your tests might never trigger.
Because nothing executes, analysis is fast and safe to run on every save. The next section shows the kind of bug it finds that a quick eyeball would miss.
2️⃣ Catching Bugs & Type Inference
The core power is type inference : the tool works out the type of every value from how it's assigned and used, then checks each operation against it. If getName() returns a string , calling ->save() on the result is impossible — and the analyzer says so, pointing at the exact line, before you ever run the file.
That bug would only surface at runtime on the specific path that calls greet() . Static analysis surfaces it immediately, everywhere, for free.
3️⃣ Guiding the Analyzer with PHPDoc
PHP's native types can't say what's inside an array. PHPDoc annotations — @param , @return , @var — declare richer types in comments that the analyzer reads and enforces. The runtime ignores them, but PHPStan and Psalm use them to check every call site.
Now the tool knows find() returns an array<string, mixed> and that $names is a list<string> — far more than the bare array the engine sees.
4️⃣ Generics in Docblocks
PHP has no native generics , but the analyzers support them in PHPDoc . Write @return list<User> or @return array<int, User> and the tool tracks the element type through loops and calls — so accessing $u->name on each item is verified as valid.
This is how you get type-safe collections in PHP today: native code stays plain array , while the docblock gives the analyzer the precise element type to check against.
5️⃣ Config, Levels, Baselines & CI
You configure the tools in a file: phpstan.neon (NEON format) for PHPStan, psalm.xml for Psalm. There you set the strictness level (PHPStan 0–9; Psalm has errorLevel 1–8, lower = stricter), the paths to scan, and any rules to ignore. A new project can start at the strictest setting; a legacy one starts low and climbs.
For big existing codebases, generate a baseline ( --generate-baseline ): it records today's errors so only new ones break the build. Then add the analyzer to CI so every pull request is checked automatically — analysis becomes a gate, not a chore. Remember it complements, not replaces, your tests : analysis proves type and flow correctness, tests prove behavior.
Now you try the two core skills — adding the right PHPDoc, and configuring/running the tool.
Fill in each ___ using the 👉 hint, then check it against the Output panel.
One more. Set the strictness and run the analysis from the command line.
📋 Quick Reference — Static Analysis
No code is filled in this time — just a brief and an outline. Write a small typed class, point PHPStan at it at the strictest level, and fix every error until the report is clean. This is exactly the loop you'll run on real projects.
Practice quiz
What is static analysis?
- Inspecting source code for bugs without executing it
- Profiling memory usage at runtime
- Minifying code for production
- Running your test suite repeatedly
Answer: Inspecting source code for bugs without executing it. Static analysis reads and reasons about your code without running it, catching whole classes of bugs (type errors, undefined methods) before the program ever executes.
Which two tools are the standard static analyzers for PHP?
- PHPUnit and Pest
- Composer and Packagist
- PHPStan and Psalm
- Xdebug and Blackfire
Answer: PHPStan and Psalm. PHPStan and Psalm are the two leading static analysis tools for PHP. Both find type errors and bugs by analyzing code without running it.
What do PHPStan's levels (0 to 9) control?
- How long the analysis runs
- How strict the checks are, with higher numbers catching more potential issues
- How many files it scans
- How many CPU cores it uses
Answer: How strict the checks are, with higher numbers catching more potential issues. PHPStan levels run from 0 (loosest) to 9 (strictest). Each level adds more rules, so you can adopt the tool gradually and raise the bar over time.
What does type inference let a static analyzer do?
- Run the code to see what type appears
- Rename your classes automatically
- Guess variable names
- Work out the type of a value from how it is assigned and used, even without an annotation
Answer: Work out the type of a value from how it is assigned and used, even without an annotation. Type inference means the tool figures out a value's type from context, so it can flag a mismatch even where you did not write an explicit type.
What is a PHPDoc annotation like @param string $name used for?
- It documents and declares types in a comment, which the analyzer reads to check usage
- It changes the runtime behavior of the function
- It generates HTML documentation only
- It is ignored by everything
Answer: It documents and declares types in a comment, which the analyzer reads to check usage. PHPDoc tags such as @param, @return, and @var declare types in comments. Static analyzers read them to verify how values flow through your code, including types PHP's native syntax cannot express.
What is the purpose of a baseline file?
- It deletes all existing errors
- It records the current known errors so the tool only reports NEW ones, letting you adopt analysis on legacy code
- It speeds up the analysis
- It stores your database schema
Answer: It records the current known errors so the tool only reports NEW ones, letting you adopt analysis on legacy code. A baseline snapshots existing errors so they are suppressed; the tool then only fails on new issues. This lets a big legacy codebase adopt strict analysis without fixing everything at once.
Where do you configure PHPStan and Psalm respectively?
- composer.json only
- phpstan.json and psalm.json
- config.php for both
- phpstan.neon and psalm.xml
Answer: phpstan.neon and psalm.xml. PHPStan reads phpstan.neon (NEON format) and Psalm reads psalm.xml. Each defines which paths to scan, the strictness level, and any ignored rules.
How are generics expressed in PHP static analysis?
- Only inside .neon files
- With native PHP syntax like List<int>
- In PHPDoc, for example @return array<int, User> or @param list<string> $names
- They are not supported at all
Answer: In PHPDoc, for example @return array<int, User> or @param list<string> $names. PHP has no native generics, so analyzers read them from PHPDoc, e.g. @return array<int, User> or Collection<User>, giving precise element types the engine itself does not track.
What is the recommended way to keep code passing analysis over time?
- Run it once before release and never again
- Run the analyzer in your CI pipeline so every push and pull request is checked
- Only run it on your own machine
- Disable it whenever it fails
Answer: Run the analyzer in your CI pipeline so every push and pull request is checked. Adding the analyzer to CI means every commit is automatically checked, so type errors and bugs are caught in review rather than in production.
How does static analysis relate to your tests?
- It complements tests: analysis proves type and flow correctness, tests prove behavior is correct
- It is the same thing as testing
- Tests make analysis unnecessary
- It replaces unit tests entirely
Answer: It complements tests: analysis proves type and flow correctness, tests prove behavior is correct. Static analysis and tests cover different things. Analysis catches type mistakes and impossible code paths without running anything; tests verify actual behavior. You want both.