Glossary
Static code analysis: errors that can be found before execution
Tools such as PHPStan and Psalm complement tests with fast checks of types and possible errors across the codebase.
Short definition
Checking source files without running the application.
An analyser uses type declarations, method signatures, PHPDoc, available classes, and possible paths through the program. It can flag a method call on a potentially null value, an incorrect argument type, a missing method, or unreachable code.
It cannot verify whether a business rule matches the requirements or whether an external API really responds correctly. Static analysis does not replace tests, monitoring, or code review; it looks for a different kind of risk, which is precisely why they complement one another.
Use cases
What analysis is used for
It is particularly useful when maintaining and refactoring a system with many interfaces and data sources.
- types of arguments, return values, properties, and collections
- nonexistent classes, methods, properties, and invalid calls
- nullable values used without a prior check
- more precise contracts for DTOs, repositories, and generic helper classes
- checking pull-request changes before merging into the main branch
Practical example
A missing customer during import
A repository can return ?Customer because no customer exists for the given external ID. If the import continues without a check and reads the customer’s email, the analyser at a higher level flags a method call on a potentially null value.
The correct fix is not to silence the message. The application must express the intended behaviour: create the customer, mark the import incomplete, or use a method whose contract throws a specific exception when the customer is not found. Analysis does not invent the business decision, but forces it to become visible.
How it works
From type information to diagnostics
Accuracy depends on how well the project describes its actual contracts.
- Input PHP files, Composer autoloading, native types, PHPDoc, and framework configuration.
- Model The tool builds a model of known classes, methods, properties, and possible value types.
- Code traversal It evaluates possible branches and verifies whether operations on values of the given types are safe.
- Diagnostics The result identifies the file, line, and reason for the inconsistent contract.
- CI gate A new error can cause the check to block a pull request or merge.
What is what
Static analysis is more than linting
Teams use the word lint differently, so it is worth naming the particular kind of check.
Syntax lint
php -l verifies that the parser can load a file. It does not find a call to $customer->email() when customer may be null.
Code style and architecture
ECS or PHP_CodeSniffer checks formatting, imports, and team conventions. These improve readability but do not prove correct types. Deptrac checks permitted dependencies between layers rather than the types of individual expressions.
Automated tests
Tests run the application in specific scenarios and verify rules or integrations. Analysis is faster and also traverses branches the tests do not call.
PHPStan and Psalm
These are separate analysers with their own configuration and extensions. There is no need to run both; trustworthy type information and continuous resolution of findings matter more.
Benefits and limitations
A powerful check, not infallible proof of correctness
Benefits
- type inconsistencies appear before the browser or production
- refactoring an interface reveals dependent locations
- more precise contracts help the IDE and the next developer
- the check can run quickly and repeatedly in CI
Limitations
- does not know future database data or the real behaviour of an external API
- dynamic methods, reflection, and incomplete metadata reduce accuracy
- a broad baseline and global ignores can hide new errors
- a green run does not replace a business test or security review
Scope of use
It is better to describe an error precisely than to ignore it globally.
If the analyser reports a problem in working dynamic code, a third-party type, framework support, or a precise contract at the data source is often missing. Adding a stub, extension, or native type is better than disabling an entire rule category.
A gradual transition makes sense in a large legacy system. A baseline suppresses documented existing findings; CI must fail on new ones, and the baseline should shrink over time, or it easily becomes a permanent way of disabling the check.
What to keep in mind
Analysis works only with trustworthy inputs
The check should be part of everyday work, not an occasional report without an owner.
- native types as the default contract
- precise PHPDoc for collections and generic tools
- framework extensions and configuration that match the project
- CI that blocks new errors
- a justified baseline that is continuously reduced
Common questions
What static analysis verifies
Does static analysis replace automated tests?
No. Analysis finds type and structural inconsistencies without running a scenario. A test verifies specific behaviour, rules, and collaboration with infrastructure.
What is the difference between PHPStan and PHPUnit?
PHPStan traverses source code and evaluates types and possible errors. PHPUnit runs prepared tests and checks the expected result of specific behaviour.
Does it make sense to use level max from the start?
Yes for a new, well-typed project if the team resolves findings continuously. In an older system, a gradual transition and carefully managed baseline may be more practical.
Is PHPDoc reliable enough?
It helps particularly with collections and generics, but depends on the declared contract. Correct native types and precise information at the data source therefore take priority.
How I use static analysis in practice
Every quality check covers a different kind of risk.
I use PHPStan at a high level alongside unit and integration tests as part of my regular development workflow.