PHPackages                             sanmai/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. sanmai/phpstan-rules

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

sanmai/phpstan-rules
====================

Custom PHPStan rules for enforcing code quality standards

0.3.14(3mo ago)311.5k↓29.9%[2 PRs](https://github.com/sanmai/phpstan-rules/pulls)8Apache-2.0PHPPHP ^8.2CI passing

Since Jul 3Pushed 2mo agoCompare

[ Source](https://github.com/sanmai/phpstan-rules)[ Packagist](https://packagist.org/packages/sanmai/phpstan-rules)[ GitHub Sponsors](https://github.com/sanmai)[ RSS](/packages/sanmai-phpstan-rules/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (12)Versions (26)Used By (8)

sanmai/phpstan-rules
====================

[](#sanmaiphpstan-rules)

[![License](https://camo.githubusercontent.com/b29de0acdfd19013f1f02689b15c933e4a6c145be9efa718288f88ba3280b1c5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d417061636865253230322e302d626c75652e737667)](LICENSE)[![PHPStan](https://camo.githubusercontent.com/ecb39a33957e802f1f085f1debada1e99904e72b8d807e98991fb7f9660cb6d3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d6d61782532306c6576656c2d627269676874677265656e2e737667)](https://phpstan.org/)

A collection of opinionated PHPStan rules focused on enforcing functional programming patterns and reducing complexity. These rules are tailored specifically to the kind of code LLMs are prone to produce.

Philosophy
----------

[](#philosophy)

These rules encourage:

- Functional programming patterns over imperative nested structures
- Early returns and guard clauses for better readability
- Reduced cyclomatic complexity through flatter code structures
- Explicit code flow that's easier to test and maintain

These principles align well with libraries like [`sanmai/pipeline`](https://github.com/sanmai/pipeline) that provide functional programming patterns as alternatives to nested loops.

My rules are designed to work together without creating "whack-a-mole" scenarios. When multiple rules could apply to the same code pattern, they all trigger simultaneously, showing you the complete picture upfront. For example, a function ending with if-else will trigger all matching rules, guiding you directly to the guard clause rather than making you fix one issue only to discover another. This approach helps you fix all violations in a single pass.

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

[](#installation)

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

If you also install `phpstan/extension-installer` then you're all set!

```
composer require --dev phpstan/extension-installer
```

### Manual Installation

[](#manual-installation)

If you don't want to use `phpstan/extension-installer`, include the extension in your project's `phpstan.neon`:

```
includes:
    - vendor/sanmai/phpstan-rules/extension.neon
```

Rules
-----

[](#rules)

### `NoNestedLoopsRule`

[](#nonestedloopsrule)

**Prevents nested loops within the same function scope.**

This rule encourages extracting nested loops into separate methods or using functional approaches like `array_map()`, `array_filter()`, or the `sanmai/pipeline` library.

#### Bad

[](#bad)

```
foreach ($users as $user) {
    foreach ($user->getPosts() as $post) { // Error: Nested loops are not allowed
        if ($post->isPublished()) {
            $titles[] = $post->getTitle();
        }
    }
}
```

#### Good - Using sanmai/pipeline

[](#good---using-sanmaipipeline)

```
use function Pipeline\take;

$titles = take($users)
    ->map(fn($user) => yield from $user->getPosts())
    ->filter(fn($post) => $post->isPublished())
    ->cast(fn($post) => $post->getTitle())
    ->toList();
```

### `NoNestedIfStatementsRule`

[](#nonestedifstatementsrule)

**Discourages simple nested if statements without else branches.**

This rule promotes combining conditions with logical operators or using guard clauses for flatter code structure.

#### Bad

[](#bad-1)

```
if ($user->isActive()) {
    if ($user->hasPermission('edit')) { // Error: Nested if statements should be avoided
        $this->grantAccess();
    }
}
```

#### Good - Combined conditions

[](#good---combined-conditions)

```
if ($user->isActive() && $user->hasPermission('edit')) {
    $this->grantAccess();
}
```

#### Good - Guard clauses

[](#good---guard-clauses)

```
if (!$user->isActive()) {
    return;
}

if (!$user->hasPermission('edit')) {
    return;
}

$this->grantAccess();
```

### `RequireGuardClausesRule`

[](#requireguardclausesrule)

**Enforces the use of guard clauses in loops instead of wrapping the main logic in if statements.**

This rule encourages early returns/continues to reduce nesting and improve readability.

**Exception**: Loops where the if statement contains only `return`, `yield`, `yield from`, or `throw` statements are allowed, as these are common patterns for filtering/searching operations.

#### Bad - Loop with only if

[](#bad---loop-with-only-if)

```
foreach ($items as $item) {
    if ($item->isValid()) { // Error: Use guard clauses
        $item->process();
        $item->save();
    }
}
```

#### Good - Guard clause

[](#good---guard-clause)

```
foreach ($items as $item) {
    if (!$item->isValid()) {
        continue;
    }

    $item->process();
    $item->save();
}
```

#### Good - If with other statements (allowed)

[](#good---if-with-other-statements-allowed)

```
foreach ($items as $item) {
    if (count($buffer) >= $limit) { // OK: Loop has more than just the if
        array_shift($buffer);
    }

    $buffer[] = $item;
}
```

### `NoElseRule`

[](#noelserule)

**Forbids the use of `else` statements.**

This rule enforces the use of early returns and guard clauses instead of `else` branches, leading to flatter and more readable code.

#### Bad

[](#bad-2)

```
if ($user->isActive()) {
    return $user->getName();
} else { // Error: Else statements are not allowed
    return 'Guest';
}
```

#### Good

[](#good)

```
if (!$user->isActive()) {
    return 'Guest';
}

return $user->getName();
```

### `NoEmptyRule`

[](#noemptyrule)

**Forbids the use of the `empty()` function (except on nullable arrays).**

This rule encourages more explicit checks instead of the ambiguous `empty()` function, which can hide bugs and make code harder to understand. The only exception is nullable arrays (`?array`) where `empty()` provides a cleaner syntax than `$items === null || $items === []`.

#### Bad

[](#bad-3)

```
if (empty($data)) { // Error: The empty() function is not allowed
    return null;
}

if (empty($items)) { // Error: Use $items === [] instead
    return 'No items';
}
```

#### Good

[](#good-1)

```
// Be explicit about what you're checking
if ($data === null) {
    return null;
}

// For arrays
if ($items === []) {
    return 'No items';
}

// Exception: nullable arrays are allowed
function process(?array $items): void
{
    if (empty($items)) { // OK: cleaner than ($items === null || $items === [])
        return;
    }
    // process items...
}
```

### `NoEmptyOnStringsRule`

[](#noemptyonstringsrule)

**Forbids the use of `empty()` on string types with a specific warning about the `'0'` gotcha.**

This rule specifically targets string types and mixed types (which could contain strings) to warn about the dangerous behavior where `empty('0')` returns `true`. This has caused real bugs in production systems where users couldn't search for "0" or use "0" as a valid input.

#### Bad

[](#bad-4)

```
function validateTag(string $tag): void
{
    if (empty($tag)) { // Error: empty() on strings - empty('0') returns true!
        throw new \InvalidArgumentException('Tag cannot be empty');
    }
}

// This will throw an exception even though '0' is a valid tag!
validateTag('0');
```

#### Good

[](#good-2)

```
function validateTag(string $tag): void
{
    if ($tag === '') { // Explicit check that doesn't treat '0' as empty
        throw new \InvalidArgumentException('Tag cannot be empty');
    }
}

// For nullable strings, be explicit about both conditions
function process(?string $input): void
{
    if ($input === null || $input === '') {
        return;
    }
    // process input...
}
```

### `RequireGuardClausesInFunctionsRule`

[](#requireguardclausesinfunctionsrule)

**Requires guard clauses in functions/methods that end with a single large if statement.**

This rule detects functions with void return types (or no return type) that end with a single `if` statement containing the main logic. These should be refactored to use guard clauses with early returns, reducing nesting and improving readability.

#### Bad

[](#bad-5)

```
function processData(): void
{
    $this->initialize();

    if ($this->isValid()) { // Error: Use guard clause instead
        $this->transform();
        $this->validate();
        $this->save();
        $this->notify();
    }
}
```

#### Good

[](#good-3)

```
function processData(): void
{
    $this->initialize();

    if (!$this->isValid()) {
        return;
    }

    $this->transform();
    $this->validate();
    $this->save();
    $this->notify();
}
```

This rule only applies to functions with `void` return type or no return type. Functions that declare specific return types are exempt, for now.

### `NoCountZeroComparisonRule`

[](#nocountzerocomparisonrule)

**Forbids comparing `count()` with 0.**

This rule encourages using direct array comparisons (`=== []` or `!== []`) instead of counting elements, which is more efficient and clearer.

#### Bad

[](#bad-6)

```
if (count($items) === 0) { // Error: Avoid comparing count() with 0
    return 'No items';
}

if (count($items) > 0) { // Error: Avoid comparing count() with 0
    process($items);
}
```

#### Good

[](#good-4)

```
if ($items === []) {
    return 'No items';
}

if ($items !== []) {
    process($items);
}

// Other count comparisons are fine
if (count($items) === 1) {
    return 'Single item';
}
```

### `NoFinalClassesRule`

[](#nofinalclassesrule)

**Prevents the use of `final` keyword on classes.**

This rule discourages final classes as they often create more problems than they solve, especially for testing and mocking. The `@final` annotation provides the same benefits by preventing extension via static analysis without the runtime restrictions that interfere with testing and lead to indirection hell.

#### Bad

[](#bad-7)

```
final class UserService
{
    public function getUser(int $id): User
    {
        // ...
    }
}
```

#### Good

[](#good-5)

```
class UserService
{
    public function getUser(int $id): User
    {
        // ...
    }
}

// Or with @final annotation
/**
 * @final
 */
class UserService
{
    public function getUser(int $id): User
    {
        // ...
    }

    // Private methods are not prohibited
    private function updateUsers(): void
    {

    }

    // Just as final methods are still discretionary
    final public function getAll(): iterable
    {

    }
}
```

This rule only applies to classes. To turn off this rule for specific classes, you can use PHPStan's annotation:

```
/**
 * @phpstan-ignore sanmai.noFinalClasses
 */
final class SpecialCaseThatMustBeFinal
{
    // This final class will be ignored
}
```

If you'd rather [use `dg/bypass-finals`](https://github.com/dg/bypass-finals) for testing, you can turn off this rule entirely by adding the following to your `phpstan.neon`:

```
parameters:
    ignoreErrors:
        - identifier: sanmai.noFinalClasses
```

### `NoPublicStaticMethodsRule`

[](#nopublicstaticmethodsrule)

**Allows only one public static method per class.**

Static methods are impossible to mock in tests, making code harder to test. This rule limits classes to at most one public static method to encourage dependency injection.

#### Bad

[](#bad-8)

```
class UserService
{
    public static function create(): self // First static method is fine
    {
        return new self();
    }

    public static function validate(User $user): bool // Error: Too many static methods
    {
        return $user->isValid();
    }
}
```

#### Good

[](#good-6)

```
class UserService
{
    // Protected and private static methods are allowed
    private static function getConfig(): array
    {
        return [];
    }

    protected static function validate(): void
    {

    }
}

// Classes with private constructors are exempt (factory pattern)
class ValueObject
{
    private function __construct(private string $value) {}

    public static function fromString(string $value): self // Allowed
    {
        return new self($value);
    }

    public static function fromArray(array $data): self // Also allowed
    {
        return new self($data['value']);
    }
}
```

PHPUnit tests and test cases are excluded.

Ignoring Rules
--------------

[](#ignoring-rules)

[Please refer to the PHPStan documentation.](https://phpstan.org/user-guide/ignoring-errors)

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

[](#contributing)

Found a bug or have a suggestion? [Please open an issue.](https://github.com/sanmai/phpstan-rules/issues)

###  Health Score

47

—

FairBetter than 94% of packages

Maintenance84

Actively maintained with recent releases

Popularity29

Limited adoption so far

Community16

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 82.9% 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 ~14 days

Recently: every ~52 days

Total

17

Last Release

92d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/edcb8dde95c71b1c97c3c91e57d3548795fa2014c657744fb878e2be3b5949fc?d=identicon)[sanmai](/maintainers/sanmai)

---

Top Contributors

[![sanmai](https://avatars.githubusercontent.com/u/139488?v=4)](https://github.com/sanmai "sanmai (34 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (7 commits)")

---

Tags

devstatic analysis

###  Code Quality

TestsPHPUnit

Static AnalysisPsalm

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

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

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

###  Alternatives

[phpstan/phpstan

PHPStan - PHP Static Analysis Tool

13.9k341.8M29.6k](/packages/phpstan-phpstan)[larastan/larastan

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

6.4k43.5M5.2k](/packages/larastan-larastan)[phpstan/phpstan-symfony

Symfony Framework extensions and rules for PHPStan

78768.9M1.5k](/packages/phpstan-phpstan-symfony)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

66766.6M1.1k](/packages/phpstan-phpstan-doctrine)[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.

33320.0M375](/packages/spaze-phpstan-disallowed-calls)[staabm/phpstan-dba

2912.3M2](/packages/staabm-phpstan-dba)

PHPackages © 2026

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