pestphp/pest-plugin-phpstan
A Pest plugin that brings PHPStan static analysis into your Pest test suite, letting you run and assert PHPStan results directly from tests. Integrate type and error checks into your workflow without leaving Pest.
Install the plugin alongside PHPStan itself — the plugin is a thin bridge, not a replacement for PHPStan:
composer require --dev pestphp/pest-plugin-phpstan phpstan/phpstan
# Laravel projects: use Larastan instead of raw PHPStan rules
composer require --dev larastan/larastan
The plugin registers a --phpstan flag on the Pest binary. Your first run is:
./vendor/bin/pest --phpstan
That single command runs your test suite and hands your configured paths to PHPStan, so a green build means "tests pass and static analysis is clean."
Where to look first (in order):
vendor/pestphp/pest-plugin-phpstan/src/ — the plugin is small; read the service provider/console option registration to see exactly what flags and defaults your installed version supports.vendor/pestphp/pest-plugin-phpstan/README.md — usually the only documentation; confirm the Pest major version it targets.phpstan.neon / phpstan.neon.dist — the plugin delegates all analysis config here. If you don't have one, create it before expecting useful results.Minimal working config (phpstan.neon.dist at project root):
parameters:
level: 6
paths:
- app
- src
# Laravel only:
# includes:
# - vendor/larastan/larastan/extension.neon
First real use case: wire pest --phpstan into your pre-push habit or CI job so type regressions are caught in the same gate as behavioural regressions. Start at level 5–6 with a baseline, not level 9 on day one.
Composer script as the team-facing entry point. Don't make people remember flags:
"scripts": {
"test": "@php vendor/bin/pest",
"analyse": "@php vendor/bin/pest --phpstan",
"analyse:only": "@php vendor/bin/phpstan analyse --memory-limit=1G"
}
CI: split the signals, keep the combined gate. Use the plugin for the "does everything pass" job, but emit PHPStan output separately so failures are attributable:
- run: ./vendor/bin/phpstan analyse --no-progress --error-format=github
- run: ./vendor/bin/pest --phpstan --ci
If you only have one step, at minimum capture the exit code and the tail of output — combined runs can make it ambiguous whether tests or analysis failed.
Baseline-first adoption. On an existing codebase, generate a baseline rather than lowering the level:
./vendor/bin/phpstan analyse --generate-baseline
# commit phpstan-baseline.neon, reference it in phpstan.neon
Then ratchet the level up over time and prune the baseline during refactors. A baseline that only shrinks is a healthy one.
Laravel/Larastan specifics. Include the Larastan extension, bootstrap the framework for accurate container/model resolution, and expect to need a higher memory limit:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 6
paths: [app]
bootstrapFiles:
- vendor/larastan/larastan/bootstrap.php
Complement, don't duplicate, Pest's own features. Arch tests (arch()->preset()->laravel()) cover naming/structure; PHPStan covers types and nullability; pest-plugin-type-coverage covers the ratio of typed code. Run all three, but don't re-implement one in another.
Local loop discipline. Run pest --phpstan before opening a PR; run raw phpstan analyse path/to/File.php while iterating on a single file (faster feedback than the combined command). Inline suppressions are the escape hatch, not the strategy:
/** @phpstan-ignore-next-line */
$this->legacyService()->doThing();
Prefer targeted ignoreErrors entries in phpstan.neon (with path and message regexes) over blanket ignores.
Parallelism is two separate knobs. Pest's --parallel controls test workers; PHPStan's parallelism lives in phpstan.neon (parallel.maximumNumberOfProcesses). Don't assume one tunes the other.
composer.json, and be prepared to fall back to running phpstan and pest as two separate commands if a Pest upgrade breaks the flag. Have that fallback in CI from day one.composer update, immediately re-run pest --phpstan — a silently ignored flag is the most common symptom of a mismatch. Verify with ./vendor/bin/pest --help | grep phpstan.--filter and --group do not scope analysis. Those are Pest test selectors; PHPStan analyses whatever paths says in phpstan.neon. If you expect "analyse only the files I'm testing," you'll be surprised — use phpstan analyse <path> directly for that.--memory-limit=1G or memory_limit in phpstan.neon; otherwise you'll get a fatal error that looks like a plugin crash.tmpDir (default under sys_get_temp_dir()). When results look impossible — phantom errors, or errors that won't clear after a fix — delete the cache dir or run with --debug. In CI, avoid caching PHPStan's tmp dir across jobs unless you're sure of the key.--error-format=github or --no-progress when you need machine-readable output.reportUnmatchedIgnoredErrors: true (the default) will fail the build when a baselined error disappears. That's useful — it forces baseline pruning — but it surprises people mid-refactor. Know the setting before you file a bug against the plugin.@phpstan- annotations, stub files, and services: in phpstan.neon all work exactly as in vanilla PHPStan, because the plugin doesn't intercept them. If you need behaviour the plugin doesn't expose, write it as a PHPStan rule/config — don't wait for a plugin release.phpstan.neon.vendor/pestphp/pest-plugin-phpstan/src is a few hundred lines at most and is the authoritative answer for your locked version.How can I help you explore Laravel packages today?