Question

PHP Static Code Analysis: Finding Bugs Beyond Syntax

phpcode-analysisstatic-analysis

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.

Take Quiz

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 if, calling mail() with null would violate the intended contract. A static analyzer uses the ?string type to require a null check before code that needs a real string.

A clearer version uses a guard clause:

<?php

declare(strict_types=1);

function sendReceipt(?string $email): void
{
    if ($email === null) {
        return;
    }

    echo "Sending receipt to {$email}\n";
    mail($email, 'Receipt', 'Thank you');
}

The analyzer can now infer that $email is a string after the early return.

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:

  1. The parameter type ?int means $percentage is either an integer or null.
  2. The if condition checks the null possibility.
  3. If it is null, the function immediately returns 'No discount'.
  4. Any code after that return can only run when $percentage is an int.
  5. Therefore, concatenating $percentage with '% off' follows the function's intended logic.

Now compare code that uses a variable only inside one branch:

<?php

if ($isMember) {
    $discount = 10;
}

echo $discount;

If $isMember is false, $discount was never assigned. An analyzer can flag this possible undefined-variable use. Make every path assign a value, or return early when no value should be displayed:

<?php

$discount = 0;

if ($isMember) {
    $discount = 10;
}

echo $discount;

Real World Use Cases

Static analysis is useful wherever PHP code changes frequently or handles important data.

  • Web applications: Detect a controller passing null to 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.
Take Quiz

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:

function createAccount(array $input): Account
{
    if (!isset($input['email']) || !is_string($input['email'])) {
        throw new InvalidArgumentException('A valid email is required.');
    }

    return new Account($input['email']);
}

Run analysis in continuous integration

Teams commonly run an analyzer after dependencies are installed and before merging a pull request:

vendor/bin/phpstan analyse

Start at a manageable strictness level for an existing codebase. Fix findings over time, then raise the level. If a warning is truly unavoidable, prefer a small, documented suppression at the precise location instead of disabling a broad class of checks.

Use separate tools for separate jobs

A static analyzer checks correctness assumptions. A coding-standard tool checks conventions such as spacing, naming, and brace placement. Many projects run both, plus automated tests. None replaces the others.

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['email'])) {
    throw new InvalidArgumentException('Missing email.');
}

$email = $input['email'];

Ignoring null possibilities

Broken code:

function upper(?string $name): string
{
    return strtoupper($name);
}

Handle the nullable case first:

function upper(?string $name): string
{
    if ($name === null) {
        return '';
    }

    return strtoupper($name);
}

Hiding warnings without understanding them

Suppressions can be appropriate for a known false positive, but a broad ignore rule can conceal future defects. First check whether a better type declaration, PHPDoc annotation, or validation step makes the code clear to both the analyzer and human readers.

Raising strictness too quickly in a legacy project

Enabling the strictest settings on thousands of untyped files can create an unmanageable list. Analyze a small directory first, establish a baseline, fix new code immediately, and steadily improve older code.

Comparisons

Tool or techniqueMain purposeRuns the program?Example finding
PHP linting (php -l)Parse and syntax validationNoMissing ;, malformed PHP syntax
Static analysisType, control-flow, and data-flow checksNoPossibly undefined variable or invalid argument type
Code-style checkingTeam conventions and formattingNoIncorrect indentation or naming rule
Automated testsVerify behavior with chosen inputsYesCheckout total is calculated incorrectly
Runtime error handlingHandle failures while the app runsYesDatabase connection failed

Static analysis and tests complement each other. Analysis examines many possible code paths from source structure; tests prove behavior for the cases you execute. Use both.

For PHP tools, PHPStan and Psalm are static analyzers. PHP_CodeSniffer and PHP-CS-Fixer are commonly used for style rules and formatting. Choose tools based on the checks your project needs rather than expecting one command to cover every concern.

Take Quiz

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 -l checks syntax; it is not full static analysis.
  • ?Type means Type or null.
  • 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.

Take Quiz

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 id and a non-empty string name.
  • 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.
Take Quiz

Keep learning

Related questions

View more