code quality and tooling

Psalm

Psalm is a static analysis tool for PHP. Like PHPStan, it checks code without running the application. It is especially known for detailed type analysis, rich PHPDoc support, and optional security-focused taint analysis.

Many teams choose either PHPStan or Psalm. Some use both, but most projects pick one primary analyser to avoid duplicated noise. As a junior developer, the important skill is reading the tool output and improving the code or type information.

Running Psalm

In a Composer project, Psalm is usually run from vendor/bin.

vendor/bin/psalm

Projects often wrap it in composer.json.

{
  "scripts": {
    "psalm": "psalm"
  }
}

Then the command is:

composer psalm

Use the command configured by the repository.

Configuration

Psalm normally uses an XML config file called psalm.xml.

<?xml version="1.0"?>
<psalm errorLevel="4">
    <projectFiles>
        <directory name="src" />
        <directory name="tests" />
    </projectFiles>
</psalm>

The config tells Psalm which files to analyse and how strict it should be.

Error levels

Psalm uses error levels to control strictness. Lower numbers are stricter, so level 1 is stricter than level 8.

That is the opposite direction from PHPStan's level numbers. Do not mix them up in conversation or configuration.

Existing codebases often begin at a more forgiving level, fix problems gradually, and tighten later.

Psalm understands detailed PHPDoc

Psalm can use array shapes, lists, templates, literal strings, non-empty strings, and many other PHPDoc refinements.

PHP example
<?php

declare(strict_types=1);

/**
 * @param list<array{name: string, price: int}> $products
 */
function totalPrice(array $products): int
{
    $total = 0;

    foreach ($products as $product) {
        $total += $product['price'];
    }

    return $total;
}

This helps Psalm reason about arrays that native PHP can only describe as array.

Reading a Psalm issue

A Psalm issue usually names the problem type and points to the file and line.

InvalidReturnStatement - src/ProductReport.php:12
The inferred type 'int' does not match the declared return type 'string'

That means the code's return type and actual return value disagree.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return $product['price'];
}

The fix is to make the return value match the contract.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return 'GBP ' . number_format($product['price'] / 100, 2);
}

Baselines

Psalm can use a baseline so existing issues in a legacy project do not block adoption immediately.

vendor/bin/psalm --set-baseline=psalm-baseline.xml

The baseline should not become a permanent dumping ground. Treat it as a list of debt to reduce over time.

Suppressions

Psalm supports suppressing specific issues, but suppression should be a last resort.

Try to fix the cause first:

  • add a missing native type
  • improve PHPDoc
  • check for null
  • validate an array shape
  • split unclear code into smaller functions

Suppress only when the code is correct and Psalm cannot reasonably infer it.

Taint analysis

Psalm can also perform taint analysis. Taint analysis tracks untrusted input as it moves through code and warns when it reaches sensitive output, such as SQL, HTML, shell commands, or file paths.

You do not need to master taint analysis yet, but recognise why it matters: it can find security risks that simple type checks do not cover.

Psalm or PHPStan?

The practical answer is: use the tool the project uses. Both are respected. Both catch real bugs. Both reward accurate types and PHPDoc.

When joining a team, look for:

  • psalm.xml
  • phpstan.neon
  • Composer scripts
  • CI jobs
  • baseline files
  • framework-specific plugins

Then run the same tool the project runs.

What to remember

Psalm is a static analyser with strong type and PHPDoc support. Its error levels become stricter as the number gets lower, it commonly uses psalm.xml, and it can use baselines and taint analysis.

Before moving on, make sure you can explain how Psalm differs from PHPStan at a practical level and how to fix a return-type mismatch reported by Psalm.

Practice

Task: Fix A Psalm Return Type Error

Fix the code based on this Psalm-style issue.

InvalidReturnStatement - src/ProductReport.php:12
The inferred type 'int' does not match the declared return type 'string'
PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return $product['price'];
}

echo productPriceLabel(['name' => 'Notebook', 'price' => 499]);

Requirements

  • Keep the function return type as string.
  • Keep the PHPDoc array shape accurate.
  • Return a formatted price string.
  • Include the expected output as comments.
Show solution
PHP example
<?php

declare(strict_types=1);

/**
 * @param array{name: string, price: int} $product
 */
function productPriceLabel(array $product): string
{
    return 'GBP ' . number_format($product['price'] / 100, 2);
}

echo productPriceLabel(['name' => 'Notebook', 'price' => 499]);

// Prints:
// GBP 4.99

Psalm reported that the function promised a string but returned an integer. The code now keeps price as integer pennies internally and returns a formatted string at the output boundary.