Announcing Psalm 7

November 2, 2026 by Daniil Gentili - 10 minute read

Cross-posted from Daniil Gentili's blog ».

Announcing the release of Psalm 7!

Featuring a simply enormous number of new features and bug fixes, such as:

Psalm 7 runs on 64-bit PHP 8.2 to 8.6. Upgrading is simple for most projects: see Upgrading below.

Security analysis: faster, always on, in a single run

Taints are now stored as a bitmask, and the taint graph is resolved over compact summaries of each function. This makes security analysis up to 10x faster.

Combined analysis (#11384) runs type checking, security analysis and dead code detection together in one run, so taint analysis is now on by default (#11399).

Taint issues are reported with a SECURITY severity, and with a rank in SARIF reports (#12068, #11383). The results no longer change between runs or with the thread count (#11946, #11952, #11953).

Deeper taint tracking

<?php
$run = function (string $c): void {
    shell_exec($c);
};
$run((string) $_GET['cmd']); // TaintedShell, through the closure

$mongo = new MongoDB\Driver\Manager('mongodb://localhost');
$mongo->executeQuery('db.users', new MongoDB\Driver\Query(['name' => $_GET['name']])); // TaintedNosql

/** @return mixed */
function body() {
    return json_decode((string) file_get_contents('php://input'), true);
}
eval((string) body()); // TaintedEval: php://input is a source

Try it on psalm.dev »

A new purity model: capabilities

@psalm-pure, @psalm-mutation-free and @psalm-external-mutation-free are replaced by capabilities, modelled on Hack's contexts and capabilities. A function-like declares exactly which side effects it may have: read-props, write-this-props, write-props, read-globals, write-globals, write-refs, io and time, with pure and impure at the two ends (#11984):

<?php
/** @psalm-capabilities time */
function today(): string {
    return date('Y-m-d'); // no issue
}

/** @psalm-pure */
function todayWrong(): string {
    return date('Y-m-d'); // ImpureFunctionCall: ... requires time
}

Try it on psalm.dev »

The new Purity Model article explains the model in full.

Purity templates

Many functions are only as pure as what they are given: array_map() is pure when its callback is. Purity templates are template parameters whose values are capability sets instead of types, like Hack's [ctx $f] contexts. A function can now be both pure and higher-order, and each call costs only what its arguments do (#11984).

With Closure[_], a function takes its purity from the closure it is given:

<?php
/**
 * @psalm-pure
 * @param Closure[_](int): int $f
 */
function apply(Closure $f, int $x): int {
    return $f($x);
}

/** @psalm-pure */
function double(int $x): int {
    return apply(fn(int $y): int => $y * 2, $x); // no issue: the closure is pure
}

/** @psalm-pure */
function noisy(int $x): int {
    return apply(function (int $y): int { echo $y; return $y; }, $x); // ImpureFunctionCall: The context is pure but function call on apply requires io
}

/** @psalm-pure */
function lengths(array $words): array {
    return array_map(fn(string $w): int => strlen($w), $words); // no issue: builtins taking callbacks work the same way
}

On a class, a purity template lets each subclass decide what its methods may do:

<?php
/** @psalm-purity-template P */
abstract class Handler {
    /**
     * @psalm-pure
     * @psalm-purity-from-template P
     */
    abstract public function handle(string $input): string;
}

/** @extends Handler[pure] */
final class Upper extends Handler {
    /** @psalm-pure */
    #[Override]
    public function handle(string $input): string {
        return strtoupper($input);
    }
}

/** @extends Handler[io] */
final class Printer extends Handler {
    /** @psalm-capabilities io */
    #[Override]
    public function handle(string $input): string {
        echo $input;
        return $input;
    }
}

/** @psalm-pure */
function viaUpper(Upper $handler): string {
    return $handler->handle('a'); // no issue: Upper binds P to pure
}

/** @psalm-pure */
function viaPrinter(Printer $handler): string {
    return $handler->handle('a'); // ImpureMethodCall: The context is pure but method Printer::handle requires io
}

Traversable, Iterator, IteratorAggregate and Generator have a purity template, for what iterating over them does. iterable has one too. A generator with a purity annotation binds it to what its body does:

<?php
/**
 * @psalm-pure
 * @param Traversable[pure]<int, int> $xs
 */
function sum(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // no issue
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @param Traversable<int, int> $xs
 */
function sumAny(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // ImpureMethodCall: The context is pure but iterating over Traversable<int, int> requires impure
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @return Generator<int, int>
 */
function numbers(): Generator {
    yield 1;
    yield 2;
}

/** @psalm-pure */
function total(): int {
    return sum(numbers()); // no issue: numbers() returns a Generator[pure]
}

See Purity templates in the Purity Model article.

Graph-based dead code detection

Unused code is now found on a graph of references (#11939). Code is used only if it can be reached from an entry point, so classes and methods referenced only by other unused code, cycles included, are reported. Exceptions that are created but never thrown are reported too (#12487):

<?php
final class Service {
    public function run(int $id): void {
        if ($id < 0) {
            new InvalidArgumentException('negative id'); // UnusedMethodCall: ... never thrown or used
        }
        echo $id;
    }
}

final class Ping {                                     // UnusedClass
    public static function ping(): void { Pong::pong(); }
}

final class Pong {                                     // UnusedClass
    public static function pong(): void { Ping::ping(); }
}

(new Service())->run(1);

Try it on psalm.dev »

--find-unused-psalm-suppress now also checks suppressions on classes and on Tainted* issues.

Type system

PHP 8.5 and 8.6

Plugins

CLI and configuration

Upgrading

For most projects, upgrading is as simple as updating the Composer dependency:

composer require --dev vimeo/psalm:^7.0 -W

Everything else is in the upgrade guide.

Most projects will notice:

Thanks

Thanks to everyone who contributed to Psalm 7, and to everyone who tested the betas and release candidates and reported issues!

Full Changelog: 6.20.3...7.0.0