php-standard-library/phpstan-extension
PHPStan extension for the PHP Standard Library (PSL) that teaches PHPStan about PSL types, generics, and collection APIs. It improves type inference, adds custom rules, reduces false positives, and catches PSL-specific errors during static analysis for safer, more reliable PHP code.
composer require --dev php-standard-library/phpstan-extension phpstan/extension-installer
If you use phpstan/extension-installer (recommended), it auto-registers via the package's extra.phpstan.includes entry — no phpstan.neon change needed. Otherwise, add the include manually:
# phpstan.neon
includes:
- vendor/php-standard-library/phpstan-extension/extension.neon
Where to look first: the extension.neon shipped in the package tells you which services/stubs it registers (return type extensions, stub files, rules). Cross-reference that with the Psl API surface you actually use (Psl\Type, Psl\Vec, Psl\Dict, Psl\Str, Psl\Json).
First use case: take a snippet like this and run vendor/bin/phpstan analyse --level max:
use Psl\Type;
$value = Type\int()->assert($input); // was `mixed`, now `int`
$list = \Psl\Vec\map($items, fn(int $i) => $i * 2); // inferred `list<int>`
Before the extension, $value is mixed and $list is list<mixed>; after it, you get concrete types and downstream errors surface. That delta is the whole point of installing it.
Type-object-first APIs. The biggest win is Psl\Type\TypeInterface<T> — its assert()/coerce() return type is what the extension narrows. Structure boundary code (HTTP input, JSON decoding, queue payloads) around Type\int(), Type\shape([...]), Type\union(...), and let the extension hand you back typed values instead of forcing casts and assert() blocks later.
$shape = Type\shape([
'id' => Type\positive_int(),
'tags' => Type\vec(Type\string()),
]);
$payload = $shape->assert($decoded); // array{id: positive-int, tags: list<string>}
Lean on generics for collection helpers. Psl\Vec\map, Psl\Dict\map, Psl\Iter\filter, Psl\Vec\filter get @template annotations from the extension + Psl stubs. This means fn(int $x): string => ... propagates list<string>. Prefer these over array_map/array_filter when you want PHPStan to keep track of key/value types.
Keep analysis level high. Most of the extension's value is lost at level 0–2. Level 6+ (or --level max) is where narrow return types from assert() actually change reported errors.
CI integration. Add vendor/bin/phpstan analyse --no-progress --error-format=github to your pipeline, and commit phpstan.neon with level: max for the new code paths that touch Psl, even if legacy parts sit at a lower level via paths/excludePaths.
Wrap with your own helpers if you need stability. If you plan to swap Psl out later, wrap Type\int()->assert(...) behind a thin Validator::int() facade annotated with @phpstan-assert — then the extension's inference survives as your abstraction, not a vendor lock-in.
Pair with phpstan/extension-installer and phpstan-strict-rules. Order rarely matters, but extension-installer's alphabetical loading is predictable; list any manual includes after it so your overrides win.
php-standard-library/phpstan-extension:^X while running PHPStan 2.x against a Psl 3.x install frequently degrades to mixed silently. Check composer show azjezz/psl php-standard-library/phpstan-extension phpstan/phpstan together before debugging anything else.phpstan/extension-installer and hand-add extension.neon, you can get duplicate service registrations or confusing precedence. Pick one.@template/@param-out stub files being visible to PHPStan. Don't add vendor/azjezz/psl to excludePaths, and don't strip vendor/ in scanDirectories.mixed fallbacks are the signal. If a call you expect to be typed returns mixed, first confirm the extension registered (phpstan --debug shows loaded configs), then confirm the Psl version supports that function — newer Psl APIs may be stubbed only in newer extension releases.\PHPStan\dumpType($x); in a method body (PHPStan 2.x) to inspect the inferred type, and --debug to see which extends/rules files loaded. If the extension isn't loading at all, phpstan analyse --debug will list only your project config.assert() vs coerce() differ subtly. assert() preserves narrowing (mixed → T); coerce() can widen or transform. Prefer assert() at boundaries where you want the type-checker to prove correctness, and coerce() only when you genuinely want normalization, or you lose the strictness win.DynamicReturnTypeExtension + DynamicStaticMethodReturnTypeExtension registrations — if you build your own library returning Psl\Type\TypeInterface<T>, mimic its patterns (register in your own extension.neon) rather than duplicating logic.array_* where a Psl equivalent exists), they're opt-in via rules: in phpstan.neon — enabling them on a large existing codebase will produce noise; adopt them per-directory first.How can I help you explore Laravel packages today?