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
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
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
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.xmlphpstan.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
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
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.