PHPackages                             haspadar/phpstan-rules - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. haspadar/phpstan-rules

ActivePhpstan-extension[Testing &amp; Quality](/categories/testing)

haspadar/phpstan-rules
======================

PHPStan design rules for immutability and structure

v0.53.1(1mo ago)220.8k↓49.2%2MITPHPPHP ~8.3.16 || ~8.4.3 || ~8.5.0CI passing

Since Mar 14Pushed 1mo agoCompare

[ Source](https://github.com/haspadar/phpstan-rules)[ Packagist](https://packagist.org/packages/haspadar/phpstan-rules)[ RSS](/packages/haspadar-phpstan-rules/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (8)Versions (183)Used By (2)

Iron PHPStan Rules
==================

[](#iron-phpstan-rules)

[![CI](https://github.com/haspadar/phpstan-rules/actions/workflows/sheriff.yml/badge.svg)](https://github.com/haspadar/phpstan-rules/actions/workflows/sheriff.yml)[![Coverage](https://camo.githubusercontent.com/70839787fe71b5f0f4615a0c5d0daba0a72322abe6b2bdc7712d110681e9da25/68747470733a2f2f636f6465636f762e696f2f67682f68617370616461722f7068707374616e2d72756c65732f6272616e63682f6d61696e2f67726170682f62616467652e737667)](https://codecov.io/gh/haspadar/phpstan-rules)[![Mutation testing badge](https://camo.githubusercontent.com/e34770db45e63fb0110970f0998a7034992efff0ef0ec572aaf075bf2197410a/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f7374796c653d666c61742675726c3d687474707325334125324625324662616467652d6170692e737472796b65722d6d757461746f722e696f2532466769746875622e636f6d25324668617370616461722532467068707374616e2d72756c65732532466d61696e)](https://dashboard.stryker-mutator.io/reports/github.com/haspadar/phpstan-rules/main)[![CodeRabbit Pull Request Reviews](https://camo.githubusercontent.com/fa0306034a0300a3628978d2a5759fb5685d30e88f36cdef1a4b170bd2cde13a/68747470733a2f2f696d672e736869656c64732e696f2f636f64657261626269742f7072732f6769746875622f68617370616461722f7068707374616e2d72756c65733f6c6162656c436f6c6f723d31373137313726636f6c6f723d464635373041266c6162656c3d436f64655261626269742b52657669657773)](https://coderabbit.ai)

Installation
------------

[](#installation)

```
composer require --dev haspadar/phpstan-rules
```

Then include the rules in your `phpstan.neon`:

```
includes:
    - vendor/haspadar/phpstan-rules/rules.neon
```

---

Rules
-----

[](#rules)

### Metrics

[](#metrics)

RuleDefaultDescription`MethodLengthRule`100Method body must not exceed N lines`FileLengthRule`1000File must not exceed N lines`TooManyMethodsRule`20Class must not have more than N methods`TooManyFieldsRule`5Class must not have more than N fields (declared properties + promoted ctor params)`ParameterNumberRule`3Method must not have more than N parameters`CyclomaticComplexityRule`10Method cyclomatic complexity must not exceed N`CognitiveComplexityRule`10Method cognitive complexity must not exceed N (nesting is penalised)`NPathComplexityRule`200Method NPath complexity must not exceed N (Nejmeh 1988, multiplicative)`CouplingBetweenObjectsRule`15Class must not depend on more than N unique types`BooleanExpressionComplexityRule`3Method must not have more than N boolean operators in a single expression`ClassLengthRule`500Class body must not exceed N lines`StatementCountRule`30Method must not have more than N executable statements`WeightedMethodsPerClassRule`50Sum of cyclomatic complexities of all methods must not exceed N`AfferentCouplingRule`14Class must not be referenced by more than N other classes in the codebase`InheritanceDepthRule`3Class must not extend a chain of more than N ancestors`LackOfCohesionRule`1Class methods must not split into more than N disjoint LCOM4 groups### Design

[](#design)

RuleDescription`FinalClassRule`All concrete classes must be `final``MutableExceptionRule`Exception classes must not have non-readonly properties`ReturnCountRule`Method must not have more than 1 `return` statement (default: 1)`ProtectedMethodInFinalClassRule`Final classes must not have `protected` methods`ProhibitStaticMethodsRule`Classes must not declare `static` methods, all visibility by default; opt-in `allowNamedConstructors` permits `static fromX(): self { return new self(...); }``ProhibitStaticPropertiesRule`Classes must not declare `static` properties of any visibility`ConstructorInitializationRule`Constructor must only assign `$this->property` or call `parent::__construct()``BeImmutableRule`All non-static properties must be `readonly``KeepInterfacesShortRule`Interfaces must not declare too many methods (default: 10)`NeverAcceptNullArgumentsRule`Method and standalone function parameters must not be nullable`NeverReturnNullRule`Method and standalone function return types must not be nullable, `return null` is forbidden`NoNullAssignmentRule`Plain assignments of the `null` literal (variable, property, array element) are forbidden`NoNullablePropertyRule`Class property types must not be nullable (`?Type`, `Type|null`, `null|Type`, `null`)`NoNullArgumentRule`Passing the `null` literal to user-defined functions, methods (including static and nullsafe), or constructors is forbidden`NeverUsePublicConstantsRule`Class constants must not be public (explicitly or implicitly)### Error-prone patterns

[](#error-prone-patterns)

RuleDescription`NoParameterReassignmentRule`Method parameters must not be reassigned`IllegalCatchRule`Catching `Exception`, `Throwable`, `RuntimeException`, `Error` is forbidden`IllegalThrowsRule`Declaring `@throws Exception` or other broad types in PHPDoc is forbidden`InnerAssignmentRule`Assignment inside conditions (`if ($x = foo())`) is forbidden`ModifiedControlVariableRule`Loop control variable must not be modified inside the loop body`UnnecessaryLocalRule`Local variable assigned and immediately returned/thrown must be inlined`ConstantUsageRule`Magic numbers and strings must be defined as named constants`StringLiteralsConcatenationRule`String literal concatenation via `.` or `.=` is forbidden`TodoCommentRule`TODO, FIXME, and XXX comments are forbidden in method bodies`MissingThrowsRule`Methods must declare `@throws` for every checked exception they throw (overridden methods inherit by default)`HiddenFieldRule`Method parameter or local variable must not shadow a class property (promoted constructors excluded, parameter takes precedence over local of the same name)`RequireIgnoreReasonRule`Every `@phpstan-ignore` and `@psalm-suppress` must carry a justification (default: 5 chars, parens for PHPStan, `--` for Psalm)`MultipleVariableDeclarationsRule`Chained assignments (`$a = $b = 1`) and multiple statements on one line are forbidden (default: chained `null` chains rejected)`NestedIfDepthRule`Nested `if` depth must not exceed the configured limit (default: 1; `elseif`/`else` and `Closure` reset depth)`NestedForDepthRule`Nested loop depth (`for`/`foreach`/`while`/`do-while`) must not exceed the configured limit (default: 1; `Closure` and arrow functions reset depth)`NestedTryDepthRule`Nested `try` depth must not exceed the configured limit (default: 1; `catch`/`finally`, `Closure`, and arrow functions reset depth)`SwitchDefaultRule`Every `switch` must have a `default` case and it must be last`SimplifyBooleanExpressionRule`Comparisons with `true`/`false` literals are unnecessary and must be removed`ExplicitInitializationRule`Nullable typed properties (`?T`, `T|null`, `null|T`) must not be initialized to `= null``ThrowsCountRule`Methods must not declare more `@throws` types than the configured maximum (default: 1)`IfThenThrowElseRule``else`/`elseif` after an `if` block that ends with `throw` is forbidden`NestedSwitchRule``switch` statements must not be nested inside another `switch`### Naming

[](#naming)

RuleDefaultDescription`AbbreviationAsWordInNameRule`4Identifier must not contain more than N consecutive capital letters`VariableNameRule``^[a-z][a-zA-Z]{2,19}$`Local variable name must match the configured pattern`ParameterNameRule``^(id|[a-z]{3,})$`Method parameter name must match the configured pattern`CatchParameterNameRule``^(e|ex|[a-z]{3,12})$`Catch parameter name must match the configured pattern`ForbiddenClassSuffixRule`12 suffixesClass name must not end with a generic suffix (Manager, Helper, Util, ...)`NoActorSuffixRule`27 words, 6 ns prefixesClass ending with -er/-or must match the allowedWords whitelist, or extend a class from a framework namespace### PHPDoc style

[](#phpdoc-style)

RuleDescription`PhpDocPunctuationClassRule`PHPDoc summary of every class must end with `.`, `?`, or `!``PhpDocPunctuationMethodRule`PHPDoc summary of every method must end with `.`, `?`, or `!``AtclauseOrderRule`PHPDoc tags must appear in order: `@param` → `@return` → `@throws` (configurable)`PhpDocMissingClassRule`Every named class must have a PHPDoc comment`PhpDocMissingMethodRule`Every public method in a class must have a PHPDoc comment (configurable)`PhpDocMissingPropertyRule`Every public property in a class must have a PHPDoc comment (configurable)`PhpDocMissingParamRule`Every parameter of a method with a PHPDoc block must have a matching `@param` tag`PhpDocParamDescriptionRule`Every `@param` tag must have a non-empty description after the parameter name`PhpDocParamOrderRule``@param` tags must appear in the same order as the parameters of the method signature`ReturnDescriptionCapitalRule``@return` tag description must start with a capital letter`ParamDescriptionCapitalRule``@param` tag descriptions must start with a capital letter`NoPhpDocForOverriddenRule`Overridden methods (`#[Override]`) must not have a PHPDoc comment`ClassConstantTypeHintRule`Every class constant must have a native type declaration (PHP 8.3+)`NoLineCommentBeforeDeclarationRule``//` and `#` comments are forbidden before class, method, and property declarations`NoInlineCommentRule`Comments inside method bodies are forbidden (suppress directives with `@` are allowed)---

### Configuration

[](#configuration)

All configurable rules expose their options as PHPStan parameters under the `haspadar` namespace. Override any limit in your `phpstan.neon` without touching service definitions:

```
parameters:
    haspadar:
        testsPaths:
            - '*/tests/*'
        methodLength:
            maxLines: 50
            skipBlankLines: true
            skipComments: true
        fileLength:
            maxLines: 500
        tooManyMethods:
            maxMethods: 10
            onlyPublic: true
        prohibitStaticMethods:
            onlyPublic: true
            allowNamedConstructors: true
        parameterNumber:
            maxParameters: 5
            ignoreOverridden: false
        cyclomaticComplexity:
            maxComplexity: 5
        couplingBetweenObjects:
            maximum: 10
            excludedClasses:
                - Symfony\Component\HttpFoundation\Request
        booleanExpressionComplexity:
            maxOperators: 2
        classLength:
            maxLines: 250
            skipBlankLines: true
            skipComments: true
        statementCount:
            maxStatements: 20
        weightedMethods:
            maxWmc: 30
        returnCount:
            max: 2
        illegalCatch:
            illegalClassNames:
                - Exception
                - Throwable
        illegalThrows:
            illegalClassNames:
                - Error
                - Throwable
            ignoreOverriddenMethods: false
        phpDocPunctuationClass:
            checkCapitalization: false
        phpDocPunctuationMethod:
            checkCapitalization: false
        atclauseOrder:
            tagOrder:
                - '@param'
                - '@return'
                - '@throws'
        phpDocMissingMethod:
            checkPublicOnly: true
            skipOverridden: true
        phpDocMissingProperty:
            checkPublicOnly: true
        phpDocMissingParam:
            checkPublicOnly: true
            skipOverridden: true
        phpDocParamDescription:
            checkPublicOnly: true
            skipOverridden: true
        phpdocParamOrder:
            checkPublicOnly: true
            skipOverridden: true
        abbreviation:
            maxAllowedConsecutiveCapitals: 3
            allowedAbbreviations:
                - JSON
                - HTTP
        variableName:
            pattern: '^[a-z][a-zA-Z]{2,9}$'
            allowedNames:
                - id
                - i
                - j
                - db
        parameterName:
            pattern: '^(id|[a-z]{3,})$'
        catchParamName:
            pattern: '^(e|ex|[a-z]{3,12})$'
        constantUsage:
            ignoreNumbers:
                - 0
                - 1
            checkStrings: false
            ignoreStrings:
                - ''
        stringConcat:
            allowMixed: false
        todoComment:
            keywords:
                - TODO
                - FIXME
                - XXX
        beImmutable:
            excludedClasses:
                - App\Entity\User
                - App\Entity\Order
        interfaceMethods:
            maxMethods: 5
        forbiddenClassSuffix:
            forbiddenSuffixes:
                - Manager
                - Handler
                - Processor
                - Coordinator
                - Helper
                - Util
                - Utils
                - Utility
                - Data
                - Info
                - Information
                - Wrapper
            allowedSuffixes:
                - EventHandler
                - CommandHandler
        noActorSuffix:
            allowedWords:
                - User
                - Order
                - Number
                - Member
                - Owner
                - Customer
                - Folder
                - Header
                - Footer
                - Buffer
                - Layer
                - Marker
                - Parameter
                - Character
                - Identifier
                - Integer
                - Author
                - Visitor
                - Error
                - Color
                - Vendor
                - Vector
                - Factor
                - Actor
                - Director
                - Ancestor
                - Descriptor
            excludedParentNamespaces:
                - 'Symfony\'
                - 'Illuminate\'
                - 'Doctrine\'
                - 'Laminas\'
                - 'Yii\'
                - 'Laravel\'
            excludedClasses:
                - App\Legacy\UserManager
        missingThrows:
            skipOverridden: true
        hiddenField:
            ignoreConstructorParameter: true
            ignoreAbstractMethods: false
            ignoreSetter: false
            ignoreNames: []
        requireIgnoreReason:
            minReasonLength: 5
            allowedBareIdentifiers: []
        multipleVarDecl:
            allowChainedNull: false
        nestedIfDepth:
            maxDepth: 1
        nestedForDepth:
            maxDepth: 1
        nestedTryDepth:
            maxDepth: 1
        throwsCount:
            maxThrows: 1
        afferentCoupling:
            maxAfferent: 10
            ignoreInterfaces: true
            ignoreAbstract: true
            excludedClasses:
                - App\Kernel
        inheritanceDepth:
            maxDepth: 2
            excludedClasses:
                - Symfony\Bundle\FrameworkBundle\Controller\AbstractController
        lackOfCohesion:
            maxLcom: 1
            minMethods: 7
            minProperties: 3
            excludedClasses:
                - App\Entity\User
```

Default values match the defaults described in the rules table above. Omitting a parameter keeps the default. Diagnostic identifier for `AtclauseOrderRule`: `haspadar.atclauseOrder` (for targeted ignores, e.g. `@phpstan-ignore haspadar.atclauseOrder`).

#### `testsPaths`

[](#testspaths)

`haspadar.testsPaths` is a list of fnmatch-style patterns (`*`, `?` wildcards) that mark which files PHPStan should treat as tests. Production-oriented rules are not reported on matching files; test-oriented rules (when introduced) will only be reported on matching files. Patterns are matched against the absolute file path normalised to forward slashes, so wildcards must account for the project prefix (e.g. `'*/tests/*'`, not `'tests/*'`). The default empty list disables filtering and all rules report on all files.

`AfferentCouplingRule` also consumes `testsPaths` to keep the Ca graph production-only: classes declared in matching files are neither reported nor counted as sources of incoming references, so a production class consumed only by tests is not penalised when the test directory is part of the analysed paths.

### NoActorSuffixRule — allowedWords vs renaming

[](#noactorsuffixrule--allowedwords-vs-renaming)

When the rule reports a class like `UserDispatcher`, pick one of three fixes:

1. **Rename the class to a domain noun (preferred).** `UserDispatcher` becomes `User`, `UserEvent`, `UserNotification` — whatever the class actually *is*, not what it does.
2. **Extend `allowedWords` if the suffix is a real English noun describing an entity**, not an action. Good candidates: `Container`, `Editor`, `Monitor`, `Sensor`. Bad candidates (these are actors, not entities): `Manager`, `Controller`, `Handler`, `Dispatcher`, `Coordinator`, `Orchestrator`, `Processor`.
3. **Add a framework namespace to `excludedParentNamespaces` if the class is framework-managed** (extends a controller base, implements an event-subscriber interface, etc.). Do not put `Controller` or `Handler` into `allowedWords` for this — it defeats the rule.

Rule of thumb: if the suffix describes *what the class is*, extend `allowedWords`. If it describes *what the class does*, rename.

`allowedWords` is matched **case-sensitively** against the last PascalCase segment of the class name. PHP class names follow PascalCase convention, so entries must be capitalized (`User`, not `user`).

### RequireIgnoreReasonRule — where to put the reason

[](#requireignorereasonrule--where-to-put-the-reason)

Two different delimiters, one per tool:

```
/** @phpstan-ignore foo.bar (reason in parentheses — PHPStan 1.11+ native) */
/** @psalm-suppress FooBar -- reason after double-dash (ESLint convention) */
```

`minReasonLength` counts **trimmed** characters, so padding does not help. `allowedBareIdentifiers` skips both the reason requirement and length check — use it for self-evident project-wide suppressions.

### MissingThrowsRule — @throws inheritance for overridden methods

[](#missingthrowsrule--throws-inheritance-for-overridden-methods)

This rule replaces PHPStan's built-in `exceptions.check.missingCheckedExceptionInThrows` for class methods so that overrides and interface implementations do not have to repeat `@throws` from the parent contract.

Including `rules.neon` from this package automatically sets `exceptions.check.missingCheckedExceptionInThrows: false` — the built-in check is turned off and replaced by `haspadar.missingThrows`. Do **not** re-enable the built-in flag in your own `phpstan.neon`: both rules will then fire on the same code and you will receive duplicate errors.

Current scope: only class methods are covered. Standalone functions and PHP 8.4 property hooks are not yet checked by `haspadar.missingThrows`; if your codebase needs `@throws` enforcement there, keep those analyses through separate means until the corresponding rules are shipped.

- `skipOverridden: true` (default) — overridden/interface-implementing methods inherit `@throws` from the parent and are not required to declare it themselves.
- `skipOverridden: false` — every method must declare `@throws` for every checked exception it throws, including overrides.

---

Suppressing violations
----------------------

[](#suppressing-violations)

Suppress a single occurrence inline using `@phpstan-ignore` with the rule's identifier and a mandatory reason (enforced by `RequireIgnoreReasonRule`):

```
/** @phpstan-ignore haspadar.methodLength (legacy method, extraction tracked in #123) */
public function process(): void { ... }
```

Suppress globally for specific paths in your `phpstan.neon` — useful for generated code or third-party adapters:

```
parameters:
    ignoreErrors:
        -
            identifier: haspadar.finalClass
            paths:
                - src/Generated/
        -
            identifier: haspadar.methodLength
```

Every identifier follows the pattern `haspadar.` — for example `haspadar.tooManyMethods`, `haspadar.nestedIfDepth`, `haspadar.throwsCount`.

---

Experimental rules
------------------

[](#experimental-rules)

Some rules are not registered by default because their usefulness depends strongly on project topology. They live behind an opt-in include so adopting projects do not fail on legitimate code (for example, entry-point classes that naturally have instability `I = 1`).

To enable them, add `rules-experimental.neon` to your `phpstan.neon`:

```
includes:
    - vendor/haspadar/phpstan-rules/rules.neon
    - vendor/haspadar/phpstan-rules/rules-experimental.neon
```

RuleWhy opt-in`InstabilityRule`Absolute threshold on a relative metric; `I = 1` is normal for entry-point classes`BooleanArgumentFlagRule`Public method must not accept a `bool` parameter (Clean Code "flag argument" smell); produces false positives on typed value objects that legitimately wrap a `bool`Once enabled, configure the rule like any other:

```
parameters:
    haspadar:
        instability:
            maxInstability: 0.8
            minDependencies: 5
            ignoreInterfaces: true
            ignoreAbstract: true
            excludedClasses:
                - App\Controller\HomeController
```

---

Contributing
------------

[](#contributing)

Fork the repository, apply changes, and open a pull request.

---

License
-------

[](#license)

MIT

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance93

Actively maintained with recent releases

Popularity32

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity61

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

78

Last Release

34d ago

PHP version history (2 changes)v0.1.0PHP &gt;=8.3

v0.34.0PHP ~8.3.16 || ~8.4.3 || ~8.5.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1282194?v=4)[Konstantinas Mesnikas](/maintainers/haspadar)[@haspadar](https://github.com/haspadar)

---

Top Contributors

[![haspadar](https://avatars.githubusercontent.com/u/1282194?v=4)](https://github.com/haspadar "haspadar (814 commits)")

### Embed Badge

![Health badge](/badges/haspadar-phpstan-rules/health.svg)

```
[![Health](https://phpackages.com/badges/haspadar-phpstan-rules/health.svg)](https://phpackages.com/packages/haspadar-phpstan-rules)
```

###  Alternatives

[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.6k](/packages/larastan-larastan)[phpstan/phpstan-symfony

Symfony Framework extensions and rules for PHPStan

79173.3M2.0k](/packages/phpstan-phpstan-symfony)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

67070.7M1.3k](/packages/phpstan-phpstan-doctrine)[shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.

4813.1M82](/packages/shipmonk-dead-code-detector)[spaze/phpstan-disallowed-calls

PHPStan rules to detect disallowed method &amp; function calls, constant, namespace, attribute, property &amp; superglobal usages, with powerful rules to re-allow a call or a usage in places where it should be allowed.

33321.8M509](/packages/spaze-phpstan-disallowed-calls)[mglaman/phpstan-drupal

Drupal extension and rules for PHPStan

20830.6M167](/packages/mglaman-phpstan-drupal)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
