wyrihaximus/phpstan-rules-wrapper
WyriHaximus PHPStan Rules Wrapper bundles popular PHPStan rulesets—ergebnis, strict, deprecation, PHPUnit, Mockery, ShipMonk, Symplify, PSR-3 and more—into one Composer package. Install it and, thanks to phpstan/extension-installer, the included rules work out of the box.
Install (2 commands):
composer require --dev wyrihaximus/phpstan-rules-wrapper
composer config allow-plugins.phpstan/extension-installer true
The second step matters: this package is a meta-package whose entire value is that phpstan/extension-installer auto-loads ~11 rule sets. If the plugin isn't allowed, Composer silently skips it and you get none of the rules — with no error telling you why.
First run:
vendor/bin/phpstan analyse --memory-limit=1G
If you have no phpstan.neon yet, PHPStan runs at level 0 with the bundled rulesets active. On an existing Laravel app this will flood you with hundreds of errors — that's expected, and it's the whole point of the first use case: hardening a legacy app incrementally.
Where to look first, in this order:
vendor/wyrihaximus/phpstan-rules-wrapper/ — the .neon files that include: each rule set. This is the authoritative list of what you're actually getting (the README list lags releases).vendor/ergebnis/phpstan-rules/rules.neon and vendor/shipmonk/phpstan-rules/ — the two most opinionated contributors.vendor/bin/phpstan dump-parameters — prints the fully merged config, i.e. exactly which parameters (typeCoverage, deadCode, level) are in effect after everything is loaded.First real use case: don't fight the ruleset — baseline it and ratchet.
# phpstan.neon
includes:
- phpstan-baseline.neon
parameters:
level: 6
paths:
- app
- routes
excludePaths:
- app/Console/Kernel.php
vendor/bin/phpstan analyse --generate-baseline
Now CI is green, the baseline freezes existing debt, and every new file is held to the full wrapper standard. This is how the package is meant to be consumed day to day.
Baseline-and-ratchet (the primary workflow). Regenerate the baseline only when you've genuinely fixed a chunk of errors, and regenerate it deliberately:
vendor/bin/phpstan analyse --generate-baseline=phpstan-baseline.neon
Never let CI regenerate it. Treat phpstan-baseline.neon as a shrinking file and review its diff in PRs — a baseline that grows is a regression, even though the build stays green.
Split configs for different risk profiles. The bundled rules are heavy and opinionated; applying them to tests/ is usually counterproductive. Use two invocations:
# Strict: wrapper rules + high level, application code only
vendor/bin/phpstan analyse -c phpstan.neon
# Loose: level 5, tests + factories + seeders
vendor/bin/phpstan analyse -c phpstan-tests.neon
The wrapper's extensions are global once installed, so the tests config mostly wins by narrowing paths and by ignoring error identifiers (see below).
Suppress by identifier, not by message. Wrapper rules all emit stable identifiers (PHPStan 1.10+). This survives rule message rewording and dependency bumps:
parameters:
ignoreErrors:
- identifier: ergebnis.noFinal
path: app/Http/Middleware/*
- identifier: shipmonk.deadCode.unusedClassConst
paths:
- app/Support/Legacy/*
Discover identifiers with --error-format=json and inspecting message/identifier, or just run -vvv.
CI wiring with a cache, otherwise the multi-ruleset run is painfully slow:
- run: composer install --no-interaction --prefer-dist
- uses: actions/cache@v4
with:
path: /tmp/phpstan
key: phpstan-${{ hashFiles('composer.lock', 'phpstan*.neon') }}
- run: vendor/bin/phpstan analyse --memory-limit=1G --error-format=github
--error-format=github gives inline PR annotations for free — this is what makes the wrapper's noisy rules tolerable in review.
Local pre-commit on changed files only. Full-repo runs are too slow for a hook; scope to staged files:
git diff --name-only --cached --diff-filter=ACM \
| grep -E '^app/.*\.php$' \
| xargs -r vendor/bin/phpstan analyse --no-progress
Pair it with a custom rule for house style. Because the wrapper registers its rules as services via NEON, your own rule slots in the same way — but exclude it from its own analysis if it lives under a scanned path:
services:
- App\PHPStan\NoDirectEnvRule
parameters:
excludePaths:
- analyse: app/PHPStan
Tune the parameterised rulesets explicitly rather than accepting defaults. tomasvotruba/type-coverage and the shipmonk rulesets expose parameters such as typeCoverage.minCoverage; set them to a number you currently pass, then raise by 5% per month.
The extension-installer trap. In production installs and some CI images, composer install --no-dev --no-plugins or a restrictive allow-plugins silently disables every bundled ruleset. You'll see analysis "passing" trivially. Verify with vendor/bin/phpstan dump-parameters | grep -i -E 'ergebnis|shipmonk|typeCoverage' — if nothing shows, the plugin didn't run.
Version coupling is the biggest maintenance risk. This wrapper releases continuously to track PHPStan majors — the changelog shows PHPStan 1→2 jumps (10.0.0), ergebnis being dropped then re-added (10.0.0 / 10.1.0), and thecodingmachine packages removed entirely (8.0.0). Pin it:
"require-dev": {
"wyrihaximus/phpstan-rules-wrapper": "^14.5"
}
and read the changelog before any minor bump. A single composer update can introduce dozens of new rule violations across the codebase.
Upstream has disabled rules for a reason. Release 10.14.0 explicitly turned off ergebnis' noNamedArgument and noNullableReturnTypeDeclaration. If you see other ergebnis rules that fight your codebase (Laravel leans hard on named arguments and nullable returns in service providers), don't disable the whole package — ignore by identifier per path.
Laravel magic generates false positives. The shipmonk/dead-code-detector ruleset is the most frequent offender:
ServiceProvider $bindings/$singletons properties are invoked dynamically.$casts, $fillable, $hidden are read by the framework, not by your code.__call/__get chains on facades and collections swallow types, so type-coverage sees mixed.Scope the dead-code rules away from app/Models and app/Providers, or ignore their identifiers there. Adding a larastan/larastan dependency alongside this wrapper massively reduces the mixed noise from request(), config() and collect(), since Larastan ships accurate stubs.
Rule order and config merging. Everything is auto-included, so your phpstan.neon is last. That means you can override parameters, but you cannot remove a rule an extension registered — PHPStan has no "uninclude". Your only escapes are ignoreErrors with an identifier, or excluding the extension from the installer in your root composer.json. If a bundled ruleset is fundamentally incompatible, the pragmatic path is to require the specific rule packages you want instead of the wrapper.
reportUnmatchedIgnoredErrors will bite you. The moment you fix an error covered by an ignoreErrors entry, the build turns red again with "Ignored error pattern was not matched". Keep the default true — it forces you to delete dead suppressions, which is a good thing — but don't set it to false to "simplify CI" and then accumulate permanent junk.
Peripheral rulesets need their dependencies present. phpstan/phpstan-mockery and phpstan/phpstan-phpunit only do useful work if mockery/mockery and phpunit/phpunit are actually installed; staabm/phpstan-psr3 needs a PSR-3 logger implementation and an explicit $logger type hint to analyse anything. Conversely, wyrihaximus/phpstan-no-safe flags Safe\* function usage — if you don't use thecodingmachine/safe, that ruleset is inert.
Debugging a stubborn error:
vendor/bin/phpstan analyse --debug — disables the result cache so you're seeing fresh results.vendor/bin/phpstan clear-result-cache — when errors persist for a file you just fixed.-vvv — shows which config files were loaded and in what order, which explains "why is this rule on?"--error-format=json — the only reliable way to get rule identifiers for ignoreErrors.Memory and parallelism. The bundled strict rules are memory-hungry on large Laravel apps. Set --memory-limit=1G (or -1 in CI), disable Xdebug in the CI job, and if you're on a small runner add parallel: maximumNumberOfProcesses: 1 to avoid OOM-killed child processes.
How can I help you explore Laravel packages today?