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.
Pros:
extra.phpstan.includes model, so rule sets continue to register through phpstan/extension-installer without hand-maintained includes: blocks.parameters.symplify now covers modern and collections in addition to the existing sets. This reinforces the package as a single PHPStan control plane for Laravel projects: one toggle block instead of multiple neon includes.modern and collections default to true, matching the package’s “sensible defaults with explicit opt-out” model. Teams that want a broad baseline get more coverage automatically.CommandMustHaveAsCommandAttributeRule now skips Laravel commands (#351). This directly reduces noise for Laravel console commands that do not use Symfony’s #[AsCommand] attribute.ExplicitExpectsMockMethodRule, NoMockOnlyTestRule, NoRepositoryCallInDataFixtureRule, and NoDuplicateNonRepeatableAttributeRule drops overly specific or low-value checks, reducing rule-surface maintenance.collections group adds strict Collection typing, generic docblocks, repository return types, and nullable getter checks. This is valuable for Laravel apps with Doctrine-backed modules, even though Eloquent remains the dominant ORM.MockedMethodReturnMustMatchNativeTypeRule catches mocked return-type mismatches. It belongs to the opt-in mocks set, so it does not silently expand the default test-quality surface.ForbiddenNodeRule is restored as optional, preserving extensibility without forcing it on all users.Cons:
modern: true and collections: true by default mean an upgrade from 14.17.x can surface a new wave of errors even if existing toggles were pinned. The toggle block must now include these new keys explicitly.ExplicitExpectsMockMethodRule, NoMockOnlyTestRule, NoRepositoryCallInDataFixtureRule, or NoDuplicateNonRepeatableAttributeRule lose that enforcement. This is not labelled breaking, but it changes analysis outcomes and may leave stale ignore patterns.FormTypeClassNameRule moved into the naming set (#354). Projects that disabled or selectively enabled sets may see the rule appear or disappear unexpectedly.collections Is Mostly Irrelevant to Eloquent-Only Apps: For Laravel projects without Doctrine, the new collections group adds configuration and analysis surface with little practical value. It should be disabled in the fast PR lane and in Eloquent-only repos.NoPublicNonActionMethodInControllerRule May Collide with Laravel Controller Conventions: Laravel controllers commonly expose public helper methods or invokable patterns that are not Symfony route actions. The rule targets Symfony AbstractController, but its presence in the Symfony set warrants validation against Laravel controller patterns.symplify.* matrix now has more keys, all defaulting mostly to true. Typos or omitted keys silently enable more than intended.Illuminate\Contracts, Nova, Fortify, Vapor, or Blade.Low Risk:
composer require --dev symplify/phpstan-rules:^14.19 remains the primary integration step, assuming phpstan/extension-installer is present.naming-rules.neon, static-rules.neon, doctrine-rules.neon, symfony-rules.neon, phpunit-rules.neon, and related files must still be removed to avoid duplicated-include failures.modern and collections groups can be disabled explicitly during upgrade, then enabled one at a time. This keeps the incremental adoption model intact.CommandMustHaveAsCommandAttributeRule lowers the chance of false positives in Laravel console suites.Potential Challenges:
modern: false and collections: false before comparing against the pre-upgrade baseline.ignoreErrors patterns. If reportUnmatchedIgnoredErrors is enabled, stale ignore patterns can fail CI.collections Adoption Should Be Conditional: Enable only where Doctrine is actually used. Eloquent-only Laravel apps should keep it off to avoid unnecessary analysis cost and irrelevant findings.modern Is Broad: The group bundles trait, reference, dynamic-name, and foreach-ception rules. It may flag legacy Laravel code patterns that are valid but unconventional, so enable it with a baseline or a dedicated cleanup sprint.Minimal:
modern and collections are enabled by default, making the effective config auditable if teams read the notes.#[AsDoctrineListener], traits, Laravel commands, and non-public array method calls reduce known false positives.Mitigable Risks:
modern: false and collections: false during the upgrade PR, then enabling deliberately.ignoreErrors entries that referenced them.reportUnmatchedIgnoredErrors enabled in CI and cleaning the config.FormTypeClassNameRule moving to naming can change which toggle controls it. Mitigate by updating the toggle-to-rule mapping in DEVELOPMENT.md.NoPublicNonActionMethodInControllerRule may flag public helper methods. Mitigate by validating against representative Laravel controllers before enabling the Symfony set broadly.phpstan/extension-installer is absent. Keep the smoke check that intentionally violates a known rule.true. Pin every key explicitly in shared presets.modern: false and collections: false alongside the existing toggles so the pre-upgrade error count is preserved?ignoreErrors entries or docs reference them?symplify.collections: true; Eloquent-only repos should keep it off.symplify.modern: true as a cleanup initiative, or defer until legacy trait/reference/foreach patterns are triaged?symplify.mocks: true now to gain MockedMethodReturnMustMatchNativeTypeRule, or defer until Mockery usage is reviewed?NoPublicNonActionMethodInControllerRule flag valid Laravel public helper methods in our codebase, and should the Symfony set be scoped or ignored for controllers?phpstan/extension-installer a hard requirement in the template repo and CI image, and how is auto-loading verified after every upgrade?naming + static, modern/collections/mocks/ctor off) from a nightly full lane?phpstan.neon files with pinned defaults?phpstan/extension-installer. Verify presence before removing manual includes.symplify.* toggles remain orthogonal to level:. Teams can keep level: 5 while gating modern, collections, mocks, and ctor independently.symfony and symfonyConfig sets still apply to Laravel’s Symfony-based HTTP/routing layers. The new NoPublicNonActionMethodInControllerRule needs validation against Laravel controller patterns.doctrine set remains on by default, and collections now adds stricter Collection typing rules. These are valuable only in Doctrine-backed modules; Eloquent-only apps should disable collections in fast lanes.phpunit remains default-on. mocks remains opt-in, now including MockedMethodReturnMustMatchNativeTypeRule.rector-rules.neon remains opt-in. PreferGenericTemplateOnSameUnionTypeRule is a Rector-only addition and does not affect PHPStan toggle behaviour.NoPublicNonActionMethodInControllerRule is Symfony-oriented. Laravel controllers may expose public helpers; validate before enabling broadly.CommandMustHaveAsCommandAttributeRule Laravel-command skip is a direct compatibility improvement.nunomaduro/collision or barryvdh/laravel-ide-helper for that surface.Phase 1: Upgrade and Config Cleanup
composer require --dev symplify/phpstan-rules:^14.19
phpstan/extension-installer is installed.naming-rules.neon, static-rules.neon, doctrine-rules.neon, symfony-rules.neon, symfony-config-rules.neon, phpunit-rules.neon, complexity-rules.neon, configurable-rules.neon, mocks-rules.neon, ctor-rules.neon, and symplify-rules.neon from phpstan.neon.ExplicitExpectsMockMethodRule, NoMockOnlyTestRule, NoRepositoryCallInDataFixtureRule, and NoDuplicateNonRepeatableAttributeRule.vendor/bin/phpstan analyse --level=5 --error-format=json > baseline-before.json
Phase 2: Pin the Expanded Toggle Matrix
symplify.* toggle, including the new groups, to mirror the intended baseline:
parameters:
symplify:
naming: true
complexity: true
configurable: true
static: true
symfony: true
symfonyConfig: true
doctrine: true
phpunit: true
mocks: false
ctor: false
modern: false
collections: false
baseline-before.json. A small downward delta is expected if the removed rules were previously active; any upward delta indicates an unpinned or newly active rule set.Phase 3: Incremental Enablement
modern first if the team wants broader PHP modernization coverage; expect trait, reference, dynamic-name, and foreach-ception findings.collections only in Doctrine-backed services, not Eloquent-only repos.mocks when ready for MockedMethodReturnMustMatchNativeTypeRule and other mock-quality checks.ctor last; constructor-injection rules tend to produce broad churn in Laravel service classes.--generate-baseline) to absorb legacy violations without disabling whole groups.Phase 4: Documentation and Training
modern and collections, their default values, and the rule families they contain.FormTypeClassNameRule move to naming.includes: of this package are deprecated by design and now cause duplicated-include errors.^1.x remains the practical baseline.ignoreErrors patterns. Enable reportUnmatchedIgnoredErrors to catch them.rector-rules.neon remains opt-in and is unaffected by symplify.* toggles.collections rules are relevant only where Doctrine is used. They do not replace Eloquent-specific analysis.phpstan/extension-installer present; clean branch; pre-upgrade baseline captured.modern: false and collections: false + cleanup of removed-rule references, in a single commit.modern, collections only in Doctrine services, mocks, ctor.reportUnmatchedIgnoredErrors.parameters.symplify now includes modern and collections, bringing the effective matrix to twelve keys. Fewer neon files remain, but the single block requires more deliberate pinning.true, every minor bump must be reviewed for new defaults. Pin every key explicitly, including future-added keys, to avoid inheriting new enforcement silently.FormTypeClassNameRule moving to naming means the toggle-to-rule mapping must be maintained as a living document.^14.19 and review minor bumps as behaviour-changing changes due to default-on group additions and rule removals.phpstan/extension-installer is missing — still the highest-risk silent failure.ExplicitExpectsMockMethodRule, NoMockOnlyTestRule, NoRepositoryCallInDataFixtureRule, or NoDuplicateNonRepeatableAttributeRule.modern/collections findings appearing on upgrade because the groups default to true.symplify.* key controls the rule that flagged their code. The toggle-to-rules table must now cover modern, collections, and the moved FormTypeClassNameRule.collections in Eloquent-only repos and see little value or irrelevant guidance. Documentation should state the Doctrine-only relevance clearly.naming + static, with modern, collections, mocks, and ctor off) and a comprehensive nightly lane.modern and collections add runtime on every invocation for teams that do not pin them off. PHPStan parallel and result cache help, but fast lanes should explicitly disable heavy or irrelevant groups.collections only in the subset of services with Doctrine; this keeps analysis cost proportional to actual ORM usage.phpstan/extension-installer → rules never execute; analysis “passes” while enforcing nothing. Highest-severity failure because it is invisible.modern and collections default to true; an unpinned upgrade can surface a larger-than-expected error wave and block merged PRs mid-sprint.ignoreErrors entries, causing CI failure when reportUnmatchedIgnoredErrors is enabled.symplify.* keys fall back to defaults, which are mostly enabled. With more keys, the chance of accidental over-enforcement rises.NoPublicNonActionMethodInControllerRule may flag Laravel public helper methods if the Symfony set is enabled broadly.collections rules may produce findings in codebases that do not actually use Doctrine, or may be ignored where they would be valuable.modern and collections groups, the four rule removals, and the duplicated-include failure mode in the same release note that ships the upgrade. Include explicit guidance that collections is Doctrine-only and that Laravel-only repos should keep it disabled unless Doctrine is in use.How can I help you explore Laravel packages today?