Question
Is there a static analysis tool for PHP source files that can find issues beyond syntax errors?
PHP can check a file for syntax errors, but I also want to detect problems such as:
- Unused variable assignments
- Writing to an array before it has been initialized
- Code-style warnings
- Other potential defects that could be found without running the program
Short Answer
Static analysis examines PHP source code without executing it. By the end of this page, you will understand what it can detect, how type information improves results, how to add it to a PHP project, and how to act on warnings without hiding real bugs.
Concept
Static code analysis is an automated review of source code without running the application. A PHP analyzer reads your files, builds a model of variables, functions, classes, and control flow, then reports patterns that may be incorrect or hard to maintain.
PHP's built-in lint command checks whether code can be parsed:
php -l src/OrderService.php
That is useful, but it primarily catches syntax problems such as a missing semicolon or unmatched brace. Static analyzers go further. Depending on their rules and configuration, they can report:
- A variable that may not exist on every execution path
- A method call on a value that may be
null - A function argument with an incompatible type
- A return value that does not match the documented return type
- Unreachable or unused code
- Invalid array key or object-property access
- Style and formatting violations, when paired with a code-style tool
Popular PHP static-analysis tools include PHPStan and Psalm. Code-style tools such as PHP_CodeSniffer and PHP-CS-Fixer focus on formatting and convention rules rather than deeper type and data-flow analysis.
Static analysis matters because many PHP errors otherwise appear only on a particular production request, after a specific branch of code runs. Finding them during development or continuous integration makes feedback faster and safer.
A warning is not always a guaranteed runtime failure. PHP is dynamic, and an analyzer cannot always know values produced by databases, framework containers, or external APIs. Treat results as evidence to investigate: fix a genuine issue, improve the type information, or narrowly document why a case is safe.
Mental Model
Think of static analysis as an inspector reviewing a building blueprint before construction begins.
- PHP linting checks whether the blueprint uses valid drawing symbols.
- Static analysis checks whether a staircase leads somewhere, whether a door may open into a wall, and whether a room is referenced but never built.
- Code-style checking ensures the blueprint follows the team's drawing conventions.
The inspector does not watch people use the building, so it cannot discover every real-world problem. However, it can catch many structural mistakes before they become expensive.
Syntax and Examples
Run PHP's syntax checker on one file:
php -l src/Invoice.php
Install a static analyzer as a development dependency in a Composer project. For example, with PHPStan:
composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level=6
A small configuration file can define which directories should be analyzed:
# phpstan.neon
parameters:
paths:
- src
level: 6
Consider this PHP code:
<?php
declare(strict_types=1);
function sendReceipt(?string $email): void
{
echo "Sending receipt to " . $email . "\n";
if ($email === null) {
return;
}
mail($email, 'Receipt', 'Thank you');
}
The echo line is safe because string concatenation converts null to an empty string, but the order is suspicious: the function logs that it is sending a receipt before it knows there is an address. More importantly, without the , calling with would violate the intended contract. A static analyzer uses the type to require a null check before code that needs a real string.
Step by Step Execution
Here is a small example that an analyzer can reason about without running it:
<?php
declare(strict_types=1);
function displayDiscount(?int $percentage): string
{
if ($percentage === null) {
return 'No discount';
}
return $percentage . '% off';
}
Step by step:
- The parameter type
?intmeans$percentageis either an integer ornull. - The
ifcondition checks thenullpossibility. - If it is
null, the function immediately returns'No discount'. - Any code after that
returncan only run when$percentageis anint. - Therefore, concatenating with follows the function's intended logic.
Real World Use Cases
Static analysis is useful wherever PHP code changes frequently or handles important data.
- Web applications: Detect a controller passing
nullto a service that expects a user object. - API clients: Verify that code checks optional response fields before reading them.
- Payment and order flows: Catch a method returning the wrong value type before it affects totals or status changes.
- Data-import scripts: Find array keys that may not exist in an imported CSV row.
- Refactoring: Rename a method or strengthen a type declaration, then use analysis to find callers that no longer match.
- Shared libraries: Enforce public method contracts so consumers receive the types documented by the package.
- Team consistency: Apply coding-standard checks in pull requests so reviews can focus on behavior and design.
Real Codebase Usage
In a real PHP project, static analysis is usually part of the development workflow rather than a command run only when something fails.
Add types and PHPDoc gradually
Native types give analyzers reliable information:
function findUser(int $id): ?User
{
// ...
}
For arrays and generics, PHPDoc can add detail that PHP's native type system cannot express fully:
/**
* @param list<array{id: int, name: string}> $rows
* @return list<string>
*/
function namesFromRows(array $rows): array
{
return array_map(
static fn (array $row): string => $row['name'],
$rows
);
}
Prefer guard clauses
Validate uncertain input at a boundary, then let the rest of the method operate on known-good values:
{
(!([]) || !([])) {
();
}
([]);
}
Common Mistakes
Expecting linting to find semantic bugs
This command only checks syntax:
php -l src/Report.php
It will not reliably tell you that a variable can be undefined or that a method may return the wrong type. Run a dedicated analyzer as well.
Assuming every array write requires manual initialization
This is valid PHP:
$tags[] = 'php';
PHP creates $tags as an array in this situation. However, explicit initialization can make the intended type clearer, especially when values are collected across branches:
$tags = [];
$tags[] = 'php';
The more dangerous case is reading an array key that may not exist:
// Potential warning: 'email' may be absent.
$email = $input['email'];
Validate it first:
if (!isset($input['email']) || !is_string($input[])) {
();
}
= [];
Comparisons
| Tool or technique | Main purpose | Runs the program? | Example finding |
|---|---|---|---|
PHP linting (php -l) | Parse and syntax validation | No | Missing ;, malformed PHP syntax |
| Static analysis | Type, control-flow, and data-flow checks | No | Possibly undefined variable or invalid argument type |
| Code-style checking | Team conventions and formatting | No | Incorrect indentation or naming rule |
| Automated tests | Verify behavior with chosen inputs | Yes | Checkout total is calculated incorrectly |
| Runtime error handling | Handle failures while the app runs | Yes | Database connection failed |
Cheat Sheet
# Syntax check one PHP file
php -l src/File.php
# Example: install PHPStan for development
composer require --dev phpstan/phpstan
# Analyze a source directory
vendor/bin/phpstan analyse src --level=6
// A nullable value must be checked before code requiring a string.
function label(?string $value): string
{
if ($value === null) {
return 'Unknown';
}
return strtoupper($value);
}
php -lchecks syntax; it is not full static analysis.?TypemeansTypeornull.- Use guard clauses to remove invalid or nullable cases early.
- Add native parameter and return types where possible.
- Use PHPDoc for detailed array shapes and collection element types.
- Initialize collections with
[]when it improves clarity. - Validate external input before reading expected array keys.
- Run analysis locally and in CI.
- Fix the cause of a warning before considering suppression.
FAQ
What is static code analysis in PHP?
It is the process of inspecting PHP source code without executing it to detect likely bugs, type mismatches, undefined variables, and other maintainability issues.
Does php -l perform static analysis?
It performs syntax linting. It checks whether PHP can parse a file, but it does not provide the deeper type and control-flow checks of a dedicated static analyzer.
Which PHP tools perform static analysis?
PHPStan and Psalm are widely used PHP static analyzers. They can be installed with Composer and run against selected project directories.
Can static analysis find all PHP bugs?
No. It cannot fully predict database contents, network responses, user behavior, or all dynamic runtime behavior. It is best used alongside tests, logging, and runtime error handling.
Why does a static analyzer complain about an array key?
The analyzer may see a path where the key is absent. Validate external input with isset(), array_key_exists(), or a dedicated input-validation layer before accessing required data.
Should I initialize an array before using $items[] = ...?
PHP can create the array automatically for a simple append. Initializing with $items = []; is often clearer and helps communicate intent, especially in more complex control flow.
How should a legacy PHP project adopt static analysis?
Start with a small directory or a moderate strictness level. Fix new findings in changed code, gradually improve types and documentation, and increase strictness when the warning count is under control.
Mini Project
Description
Build a small function that converts untrusted product data into safe display labels. This mirrors a common API or import-task boundary: incoming arrays may be missing fields or contain values of the wrong type. Clear validation and type declarations allow a static analyzer to reason about the code.
Goal
Create product labels only from valid input rows, while safely skipping invalid rows.
Requirements
- Accept a list of input rows as an array.
- Treat a row as valid only when it has an integer
idand a non-empty stringname. - Return one label per valid row in the format
#id: name. - Skip invalid rows without producing notices or undefined-array-key errors.
- Add PHPDoc that describes the input rows and returned list.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.