symplify/phpstan-rules
A set of 80+ practical PHPStan rules for checking clean architecture, logical errors, naming, namespace placement, visibility overrides, and Symfony, Doctrine, and PHPUnit best practices—useful for any PHP project, from legacy codebases to modern stacks.
Installation:
composer require symplify/phpstan-rules --dev
composer require phpstan/extension-installer --dev
phpstan/extension-installer is now the primary integration path — the package registers its rule sets through extra.phpstan.includes, so nothing else needs to be added to composer.json.
First Configuration (no includes):
Since 14.17.0 the rule sets are auto-loaded. Do not add includes: for naming-rules.neon, static-rules.neon, symplify-rules.neon, modern-rules.neon, collections-rules.neon, etc. — that now produces duplicated-include errors in PHPStan. phpstan.neon only needs the toggles you care about:
parameters:
symplify:
symfony: false
symfonyConfig: false
doctrine: false
Anything you omit keeps its default (see the toggle table below). Note that since 14.19.0 a bare config like the one above now runs the new modern and collections groups too — they are on by default.
First Use Case: Run PHPStan on a single file, then on a small directory, and read the rule class name in each error message before touching the config again:
vendor/bin/phpstan analyse src/Controller/SomeController.php
On a Doctrine project, start with an entity or repository: the collections group gives fast, high-signal feedback (Collection generics, repository return types).
Default toggle state (14.19.0):
| Parameter | Default | Notes |
|---|---|---|
symplify.naming |
true |
Rename-oriented checks; now also owns FormTypeClassNameRule |
symplify.complexity |
true |
Structural / size checks |
symplify.configurable |
true |
Rules that read their own arguments |
symplify.static |
true |
Static analysis helpers, no framework needed |
symplify.modern |
true |
New in 14.19.0 — trait, reference, dynamic-name, foreach-ception rules |
symplify.collections |
true |
New in 14.19.0 — Doctrine Collection typing rules |
symplify.symfony |
true |
Framework-specific; noisy outside Symfony |
symplify.symfonyConfig |
true |
Validates YAML/JSON config files |
symplify.doctrine |
true |
ORM-oriented checks |
symplify.phpunit |
true |
Test-suite checks |
symplify.mocks |
false |
Opt-in; slimmer since two mock rules were removed |
symplify.ctor |
false |
Opt-in |
config/rector-rules.neon is not auto-loaded and remains opt-in via an explicit includes: entry.
Upgrade day (existing installs):
vendor/symplify/phpstan-rules/config/*.neon include from phpstan.neon, including symplify-rules.neon.composer dump-autoload (extension-installer needs the refreshed autoloader to pick up the new extra.phpstan.includes).modern and collections are newly enabled by default.Progressive Rollout / Per-Project Profiles:
parameters:
symplify:
symfony: false
symfonyConfig: false
doctrine: false
collections: false
collections only makes sense with Doctrine, so switch it off alongside doctrine on non-Doctrine codebases. Keep modern on — it is framework-agnostic and cheap to satisfy.phpstan.neon for src, a stricter one for tests) rather than toggling rule-by-rule in one file.Opting Into the Disabled Sets:
mocks and ctor are off by default; enable only when the codebase is already consistent about test doubles and constructor design:
parameters:
symplify:
mocks: true
ctor: true
mocks is narrower than it used to be (ExplicitExpectsMockMethodRule and NoMockOnlyTestRule were removed in 14.19.0), so re-measure before assuming it is still too noisy.Rule-Specific Arguments:
services in your own config. Treat the symplify.* booleans as an on/off switch and use services/rules for arguments and severity only.Rector Pairing:
rector-rules.neon must still be included manually. Keep it in a separate phpstan-rector.neon that extends your base config, so the extra passes don't run on every local analysis:
includes:
- phpstan.neon
- vendor/symplify/phpstan-rules/config/rector-rules.neon
PreferGenericTemplateOnSameUnionTypeRule there — it is a rector rule, so it does nothing unless that include is present.CI/CD Pipeline:
- name: PHPStan
run: vendor/bin/phpstan analyse --level=5 --no-progress
Because defaults are enabled, CI is now stricter than before on upgrade day — and stricter again with 14.19.0. Land the toggle config in the same PR as the composer bump.
IDE Integration:
PHPStorm reads the same phpstan.neon, so the symplify.* toggles apply to real-time feedback automatically. No IDE-side setup beyond pointing at the config.
Baseline First, Rules Second:
Generate phpstan-baseline.neon immediately after upgrading, then fix violations set by set by deleting baseline entries. With the new collections set this works especially well: generic docblocks on entities/repositories can be added class by class without blocking merges.
Doctrine Typing Sweep:
Use collections as a one-off cleanup pass over src/Entity and src/Repository with the rest of the config unchanged:
vendor/bin/phpstan analyse src/Entity src/Repository --no-progress
Fix the getters/params/properties, then re-enable normal analysis.
Duplicated Includes:
symplify-rules.neon, and now modern-rules.neon/collections-rules.neon if you copied names from the release notes) now clashes with the auto-registered one. Remove the include; do not try to "win" by ordering.New Default Sets on 14.19.0:
modern and collections are enabled by default. collections fires on Doctrine mapping classes and will look like noise if you deliberately keep doctrine: false — turn collections: false off with it rather than baselining. modern fires broadly (traits, references, dynamic names, nested foreach) and is worth keeping on.Silent Extension-Installer Failures:
symplify.* errors appear and no set seems active, the extension was never registered: check that phpstan/extension-installer is installed and that composer.json does not have "allow-plugins" blocking it, then composer dump-autoload and vendor/bin/phpstan clear-result-cache.Disabling Is Explicit — Partial Config Does Not Reset Defaults:
symplify: { nops: false } (typo) is not an error; the key is ignored and the set stays on. Only the exact documented keys take effect, so a typo looks like "the toggle doesn't work". Copy key names from the release notes rather than retyping.Rules Removed in 14.19.0 — Clean Up Your Ignores:
ExplicitExpectsMockMethodRule, NoMockOnlyTestRule, NoRepositoryCallInDataFixtureRule and NoDuplicateNonRepeatableAttributeRule are gone. Any baseline entries, ignoreErrors patterns or @phpstan-ignore-line comments targeting them become stale and trip reportUnmatchedIgnoredErrors. Grep your config for the rule class names/identifiers when upgrading.Bugfixes Can Invalidating Existing Suppressions:
NoAssertFuncCallInTestsRule now detects test classes via TestCase reflection and skips traits; ForbiddenArrayMethodCallRule allows array method calls on non-public methods; RequireQueryBuilderOnRepositoryRule skips traits; NoListenerWithoutContractRule skips #[AsDoctrineListener]; CommandMustHaveAsCommandAttributeRule skips Laravel commands; curl_* and array_walk left the forbidden-function list. Each of these removes errors you may have ignored for — expect a batch of unmatched-ignore failures, not just fewer errors.PreferInterfaceInConstructorRule and Mailer transports still applies: drop any inline ignore you kept for it.mocks and ctor Off by Default:
mocks: true / ctor: true toggles in the same commit — and re-check the set's contents, since two rules were removed from the mocking group.rector-rules.neon Is Not Auto-Loaded:
PreferGenericTemplateOnSameUnionTypeRule (new) and everything else in that file are inert without the include. Don't assume the upgrade wired it up.FormTypeClassNameRule Moved Sets:
naming group. If you had naming: false with configurable: true (or vice versa) to isolate it, your effective coverage changed after 14.19.0; re-run and diff rather than assuming the same toggles produce the same errors.Symfony-Set Noise:
symfony, symfonyConfig and doctrine are on by default and will fire on non-Symfony/Doctrine codebases. This includes NoPublicNonActionMethodInControllerRule, which now flags any public helper in a controller class. Turn the sets off up front instead of baselining hundreds of irrelevant errors.False Positives in Path-Based Rules:
Rule-Specific Errors:
Symplify\PHPStanRules\Rules\...). Search the README for that class before changing configuration, and check which set it belongs to so you know which symplify.* toggle controls it — the new rules are split across collections, modern, doctrine, symfony, naming and the opt-in mocks group.Verify What Is Actually Loaded:
vendor/bin/phpstan analyse -v plus vendor/bin/phpstan dump-parameters (or your own --debug run) show the resolved parameter tree; confirm symplify.* matches what you wrote rather than what you intended. This is the fastest way to confirm modern/collections really default to true in your install.Caching After Config Changes:
vendor/bin/phpstan clear-result-cache after any symplify: edit — mandatory after a minor bump that adds sets.Ignoring Specific Violations:
ignoreErrors with a path and message pattern in phpstan.neon over per-line @phpstan-ignore-line; per-line ignores break on refactors and trigger unmatched-ignore errors, which is exactly the failure mode the 14.19.0 removals and bugfixes create.Generic Docblock Rules Need the Mapping Visible:
Collection generics rules read Doctrine mapping attributes and existing docblocks. Excluding entity directories from analysis, or analysing a file in isolation without its use statements resolvable, hides or distorts the result. Analyse the whole src/ for a reliable count.Custom Rules:
rules: in your project config. Keep project-specific rules out of the bundled sets so a future release never changes their behaviour.Layered Configs:
phpstan.neon with the symplify.* profile for the app and includes:-based child configs (phpstan-tests.neon, phpstan-rector.neon) that only differ in toggles plus the opt-in rector-rules.neon.Severity, Not Deletion:
rules: rather than disabling the whole set — you keep visibility without blocking merges. This is the documented preference upstream: prefer group toggles over per-rule switches, and reach for severity before deletion.Treat the Toggle Block as Versioned Config:
modern and collections as default-on groups. Own a symplify: block explicitly, even if it mirrors defaults, and review it in every upgrade PR so a silent default flip cannot change your CI outcome.How can I help you explore Laravel packages today?