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

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

hihaho/phpstan-rules
====================

Hihaho PHPStan rules according to the guidelines

v3.13.0(1w ago)747.3k↓60.8%11MITPHPPHP ^8.3CI passing

Since Sep 20Pushed 1w ago5 watchersCompare

[ Source](https://github.com/hihaho/phpstan-rules)[ Packagist](https://packagist.org/packages/hihaho/phpstan-rules)[ Docs](https://guidelines.hihaho.com)[ RSS](/packages/hihaho-phpstan-rules/feed)WikiDiscussions main Synced 3d ago

READMEChangelog (10)Dependencies (84)Versions (49)Used By (1)

Hihaho PHPStan rules
====================

[](#hihaho-phpstan-rules)

[![Latest Version on Packagist](https://camo.githubusercontent.com/0e5a426205f586139014561ce81ba6063a0647f85678abc40a743d8f2588519a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f68696861686f2f7068707374616e2d72756c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hihaho/phpstan-rules)[![Tests](https://camo.githubusercontent.com/6ce7cbe69a100acf1f73ff0bdcda0f9f11b4589f8ab3a2a8f8e69e8e5bc0a56a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f68696861686f2f7068707374616e2d72756c65732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/hihaho/phpstan-rules/actions/workflows/run-tests.yml)[![PHPStan](https://camo.githubusercontent.com/f44e0f53b0081faf01b797634fae5173782d62fcc7f98c33fe422197292d023c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f68696861686f2f7068707374616e2d72756c65732f7068707374616e2e796d6c3f6272616e63683d6d61696e266c6162656c3d7068707374616e267374796c653d666c61742d737175617265)](https://github.com/hihaho/phpstan-rules/actions/workflows/phpstan.yml)[![Total Downloads](https://camo.githubusercontent.com/e83eb98c69b2529d0154f3b5bd0a684451376b9075db491525037823b097d257/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f68696861686f2f7068707374616e2d72756c65732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hihaho/phpstan-rules)[![License](https://camo.githubusercontent.com/5c912b74465a56cc4fbda66b8d7f261edde8694f341fed4994a901c44cd4e1c9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f68696861686f2f7068707374616e2d72756c65732e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)[![Laravel Compatibility](https://camo.githubusercontent.com/d41c23b23609eb1f9725144ee4c1d183b6b2ccd6043728e550043d3aff939992/68747470733a2f2f62616467652e6c61726176656c2e636c6f75642f62616467652f68696861686f2f7068707374616e2d72756c65733f7374796c653d666c6174)](https://packagist.org/packages/hihaho/phpstan-rules)

A set of PHPStan rules that enforce [Hihaho's Laravel guidelines](https://guidelines.hihaho.com/laravel.html)at analyse time. They flag `invade()` calls in app code, facade aliases outside Blade, stray debug helpers (`dump`, `dd`, `ray`, and friends) left behind in production or test paths, and unvalidated request reads — including `FormRequest` fields read outside the class's own `rules()`.

If you want the auto-fix counterparts for class-naming and route-group conventions, see [`hihaho/rector-rules`](https://github.com/hihaho/rector-rules).

Requirements
------------

[](#requirements)

- PHP 8.3 or higher
- PHPStan 2.1 or higher
- Laravel 12.x or 13.x (via `illuminate/support` and `illuminate/database`)

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

[](#installation)

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

If you have [`phpstan/extension-installer`](https://github.com/phpstan/extension-installer), that's it. The rules register themselves.

Without it, include the extension in your `phpstan.neon`:

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

Rules
-----

[](#rules)

### `NoInvadeInAppCode`

[](#noinvadeinappcode)

Flags [`invade()`](https://github.com/spatie/invade) calls inside `App\`. `invade` is a test helper for reaching into private state; it has no place in production code. Also flags `\Livewire\invade()` in any namespace; if you need `invade`, use the global one from `spatie/invade`.

```
namespace App\Services;

invade($user)->privateMethod(); // reported
```

Identifiers: `hihaho.generic.noInvadeInAppCode`, `hihaho.generic.disallowedUsageOfLivewireInvade`

### `OnlyAllowFacadeAliasInBlade`

[](#onlyallowfacadealiasinblade)

Short facade aliases belong in Blade. In PHP, use the fully qualified facade so imports stay explicit.

```
use Route;                            // reported
use Illuminate\Support\Facades\Route; // fine
```

Identifier: `hihaho.generic.onlyAllowFacadeAliasInBlade`

### Debug rules

[](#debug-rules)

Three rules that together keep debug calls out of `App\` and `Tests\`:

RuleTargetsExamples`NoDebugInNamespaceRule`Global debug functions`dump()`, `dd()`, `ddd()`, `ray()`, `print_r()`, `var_dump()``ChainedNoDebugInNamespaceRule`Method chains on Laravel types`collect()->dump()`, `$builder->dd()``StaticChainedNoDebugInNamespaceRule`Static calls on Laravel facades`Http::dump()`, `Cache::dd()`The chained and static rules use PHPStan reflection to narrow matches: they only flag methods declared by (or proxied through) the `Illuminate\`namespace, so your own domain classes with a `->dump()` method stay clean.

Identifiers: `hihaho.debug.noDebugIn{App,Tests}`, `hihaho.debug.noChainedDebugIn{App,Tests}`, `hihaho.debug.noStaticChainedDebugIn{App,Tests}`

### Request-validation rules

[](#request-validation-rules)

Four rules flag unvalidated request data. The first three flag reads from `Illuminate\Http\Request`; the fourth flags reading a field inside a `FormRequest` that the same class's `rules()` never validates. Use validated data instead: `$request->validated()`, `$request->safe()->string('key')`, or the array returned by `$request->validate([...])`.

RuleTargetsIdentifier`NoUnsafeRequestDataRule`Method calls on `Request` / `FormRequest``hihaho.validation.noUnsafeRequestData``NoUnsafeRequestHelperRule``request('key')` helper with a literal arg`hihaho.validation.noUnsafeRequestHelper``NoUnsafeRequestFacadeRule`Static calls on `Illuminate\Support\Facades\Request``hihaho.validation.noUnsafeRequestFacade``UnvalidatedFormRequestFieldRule``$this->input('key')` inside a `FormRequest`, `key` ∉ `rules()``hihaho.validation.unvalidatedFormRequestField``FormRequest` auto-validation runs on dispatch, but inherited readers still return the full payload including keys outside `rules()`, so they're flagged on `FormRequest` too. Chained `request()->input('x')` is caught by the Data rule because the receiver resolves to `Request`. Zero-argument `request()` is not flagged.

```
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Request as RequestFacade;

final class StoreUserController
{
    public function __invoke(Request $request): mixed
    {
        $request->input('name');              // reported (data)
        request('id');                        // reported (helper)
        RequestFacade::boolean('debug');      // reported (facade)

        $request->safe()->string('name');     // fine

        return $request->validate(['name' => 'required']);
    }
}
```

Reads from `$this` inside a `Request` subclass, including your own `FormRequest` bases, are exempted. The scope-class check walks the inheritance chain, so a custom `App\Http\Requests\FormRequest extends BaseFormRequest extends Illuminate\Foundation\Http\FormRequest` works without extra config. Static calls on `Illuminate\Http\Request` itself (e.g. `Request::capture()`) aren't flagged; they don't return raw input.

`UnvalidatedFormRequestFieldRule` covers that `$this`-inside-a-`FormRequest` exemption from the other side: it flags `$this->boolean('submit_redirect')` when `submit_redirect` is never declared in the same class's `rules()`. To stay high-precision it only resolves a literal `return [...]` array — a conditional, spread, `array_merge()`, returned variable, or a `rules()` it can't read statically makes the class opaque and skips it — and it skips any class that overrides `prepareForValidation()`, `validationData()`, or `all()` (including via a shared base or trait), since those rewrite the validated set. `rules()` declared on a base class is followed; nested keys match on their root segment, so a rule for `address.street` validates a read of `address`.

Out of scope: ArrayAccess (`$request['x']`), magic property access (`$request->x`), and Symfony `InputBag` property access (`$request->query->get('x')`, `->headers->get()`, `->cookies->get()`). The InputBag path is legitimate for raw header or cookie reads, but flag it in code review so it doesn't turn into a de-facto suppression channel.

#### Configuration

[](#configuration)

```
parameters:
    noUnsafeRequestData:
        namespaces:
            - App
        excludeNamespaces:
            - App\Providers         # Laravel bootstrap (default)
            - App\Http\Responses    # Fortify response contracts (default)
            # - App\Http\Resources  # opt-in: accept toArray(Request) reads
        unsafeMethods:
            # full default list in extension.neon
            - input
            - all
            - get
```

`App\Providers` and `App\Http\Responses` are default-excluded because the signatures there come from the framework (`RateLimiter::for(...)` closures, `LoginResponse::toResponse(Request)`) and there's no FormRequest to route the data through. `App\Http\Resources` is opt-in. Whether a resource should read raw request is a team call.

`UnvalidatedFormRequestFieldRule` reuses `noUnsafeRequestData.namespaces` / `excludeNamespaces` and carries its own list of single-key readers under `unvalidatedFormRequestField.accessors` (`input`, `get`, `query`, `post`, `string`, `str`, `integer`, `boolean`, `float`, `json`, `array`, `collect`, `date`, `enum`, `enums`, `file`); the full default is in `extension.neon`.

#### Adopting on an existing codebase

[](#adopting-on-an-existing-codebase)

First-run baselines are nonzero. Generate one and work it down over multiple PRs:

```
vendor/bin/phpstan analyse --generate-baseline
```

Patterns that will stay baselined (the rule can't help with them):

- Dynamic-key admin CRUD. Bulk-edit controllers looping over `$request->collect('fields')->each(...)` with schema-driven keys. Suppress inline: ```
    // @phpstan-ignore hihaho.validation.noUnsafeRequestData
    $value = $request->input($field->key);
    ```
- Pre-validation framework callbacks. Already covered by the `App\Providers` default exclusion.
- Fortify response contracts. Already covered by the `App\Http\Responses` default exclusion.
- `JsonResource::toArray(Request)`. Add `App\Http\Resources` to `excludeNamespaces` if you accept the pattern.

Safe-swap yield on the first triage runs 2-10% from field data: calls already validated inline where the flagged key is in the rules, plus FormRequest cases where the flagged key is in `rules()` and migrates to `$request->safe()->string(...)` or `$request->validated()`. The rest needs judgment. Your options are to introduce a FormRequest, extend existing rules to cover the flagged key, push validation upstream, or refactor the surrounding code. Plan on several PRs over weeks, not a one-time sweep.

Common traps:

- "Injected a FormRequest, so I'm safe." The rule fires when the FormRequest has no `rules()` (auth-only wrappers) or has rules that don't cover the flagged key. Check `rules()` before assuming it's a false positive.
- `validated()` drops keys not in `rules()`, nested props included. Reading `$request->input('interactions.$.foo')` won't migrate if only `interactions` is in `rules()`. You'll need nested rules first.
- LLM agents are unreliable for bulk triage on this rule. Reliable categorization needs AST inspection that intersects `validate()` rule keys with flagged keys; one adopter's agent caught 1 of 5 candidates. Use human review or a Rector pass.
- Livewire and Filament projects handle input through component props and form schemas, outside these rules' node targets. A low hit count is a structural fact, not proof of cleanliness. Review `mount()` and form-submit paths separately.

Rule hits in `Support` or utility namespaces often point at dead code. Grep the call graph before adding to the baseline; the fix may be a delete.

### Convention rules

[](#convention-rules)

Flag a bare `true`/`false`/`null` literal passed **positionally** as the last argument of a **first-party** method, nullsafe-method, static, or constructor call. A positional `setActive('name', false)` hides what the flag means; naming it — `setActive('name', active: false)` — makes the call self-documenting.

RuleTargetsIdentifier`PositionalFlagArgumentMethodCallRule``$obj->method(..., true)``hihaho.conventions.positionalFlagArgument``PositionalFlagArgumentNullsafeMethodCallRule``$obj?->method(..., true)``hihaho.conventions.positionalFlagArgument``PositionalFlagArgumentStaticCallRule``Klass::method(..., true)``hihaho.conventions.positionalFlagArgument``PositionalFlagArgumentConstructorRule``new Klass(..., true)``hihaho.conventions.positionalFlagArgument````
namespace App\Services;

$toggle->setActive('name', false);          // reported — name the flag: active: false
$toggle?->setActive('name', false);         // reported
StaticFlag::toggle('name', false);          // reported
new Widget('name', true);                   // reported

$toggle->setActive('name', active: false);  // fine — already named
```

This pairs with rector-rules' `FirstPartyFlagArgumentToNamedRector`, which auto-fixes the flags it can resolve with bare PHPStan. Because PHPStan rules inherit the consumer's extensions, this rule flags the rest in a larastan-equipped app — including receivers (generic or inherited properties) that rector cannot resolve. rector rewrites; this rule gates.

Scope: the **last** argument only, and only when every argument is positional (no named or spread args); the matched parameter must be named and non-variadic. The parameter need **not** be bool-typed — a bare `null` on a `?Object` or `mixed` parameter is opaque too, matching the convention and the rector fixer (which names any bare flag without a type check). The gate is on the resolved member's **declaring** class, so an `App\` class inheriting a vendor method isn't flagged against vendor-declared, non-semver-stable parameter names. Callee namespaces are configurable:

```
parameters:
    positionalFlagArgument:
        firstPartyNamespaces:
            - App
            - Database\Factories
            - Tests
```

Param names aren't semver-stable in vendor code, so only first-party callees are flagged.

#### Named-argument manifest (opt-in producer)

[](#named-argument-manifest-opt-in-producer)

rector-rules' `NamedArgumentFromManifestRector` names these flags at call sites whose receiver only resolves under larastan — the sites bare-PHPStan auto-fixers can't reach. It is inert without a JSON manifest, which this package can produce: include the opt-in extension and run analysis in your larastan-equipped project.

```
includes:
    - vendor/hihaho/phpstan-rules/named-argument-manifest.neon

parameters:
    namedArgumentManifest:
        firstPartyNamespaces:
            - App
            - Database\Factories
            - Tests
        outputPath: named-arguments-manifest.json
```

`vendor/bin/phpstan analyse` then writes `named-arguments-manifest.json` — the same detection emitted as records (`{file, line, method, argIndex, paramName, value}`) instead of errors, with no CI errors raised. It is a PHPStan Collector, not an error formatter, so it is independent of the gate rules and unaffected by your baseline (baselined sites still appear in the manifest).

`outputPath` may be nested (e.g. `.config/named-arguments-manifest.json`); the parent directory is created if it does not exist.

Reflection extensions
---------------------

[](#reflection-extensions)

### Stubbed methods

[](#stubbed-methods)

`StubbedMethodsClassReflectionExtension` teaches PHPStan about **instance** methods that exist at runtime but not in reflection — Faker custom providers (added via `__call`) and Laravel macros. Without it, calls to these resolve to "undefined method" and have to be baselined, which also hides genuine typos. With it, the configured methods resolve to their declared return type, and a misspelled name (not in the configured set) still fails analysis. Statically-called methods (e.g. facade `__callStatic`) are out of scope — the stubbed methods are modelled as instance methods.

It resolves nothing by default — each project declares its own methods via the `stubbedMethods`parameter, a map of `class name => (method name => return type)`:

```
parameters:
    stubbedMethods:
        Faker\Generator:
            videoTimeInMilliseconds: int
            validPassword: string
            timestampsOfVideoClicks: 'array'
        Illuminate\Testing\TestResponse:
            assertSeeLivewire: Illuminate\Testing\TestResponse
            fillForm: Illuminate\Testing\TestResponse
        Laravel\Nova\Fields\Number:
            onlyOnExport: '$this'
```

Return types are parsed with PHPStan's type-string resolver, so any valid PHPDoc type works (`string`, `array`, a class name for chainable assertions, etc.). A return type of `$this`, `static`, or `self` is bound to the receiver, so a stubbed **fluent** method (a `$this`-returning macro, a chainable Nova field method) keeps its chain typed (`Number::make(…)->onlyOnExport()->sortable()`) instead of widening. Stubbed methods accept any arguments, so only the method name and its return type are modelled — argument types are not checked.

Return type extensions
----------------------

[](#return-type-extensions)

### Collection `values()->all()` list typing

[](#collection-values-all-list-typing)

`CollectionListAllReturnTypeExtension` types `->values()->all()` on a `Collection`/`LazyCollection`as `list` instead of `array`. Laravel types `values()` as `static`and `all()` as `array`, neither of which carries PHPStan's list marker — so a method declared `@return list` is forced to wrap the chain in `array_values()` even though `values()`already re-keyed to a list at runtime. The list type matters: a non-list array is JSON-encoded as a JS object, silently breaking frontend consumers that expect an array.

```
/** @return list */
public function ids(Collection $users): array
{
    return $users->map(fn (User $user): int => $user->id)->values()->all(); // list, no array_values()
}
```

It is registered automatically — no configuration. Two guards keep it sound: detection is syntactic (the receiver must be a direct `->values()` call, so a chain split across variables is left alone rather than guessed), and the receiver must be a `Support\Collection`/`LazyCollection` (or subclass) — a bare `Enumerable` or a custom implementation with unknown key semantics is never narrowed.

### Route-model binding typing

[](#route-model-binding-typing)

`RouteBindingReturnTypeExtension` types `$this->route('x')` / `$request->route('x')` as the model bound to route parameter `x`, reading the actual bindings from your route-service providers. Laravel types `route()` as `object|string|null`, so resolving a bound model normally needs an `assert($model instanceof Video)` after every call — and the parameter name doesn't reveal the model (`video_id` → `Video`). This reads `Route::model('x', M::class)` and `Route::bind('x', fn (): M => …)`from the configured providers and returns the bound type, so the assert is no longer needed while a wrong assignment still fails.

Configure the providers to read via the `routeBindingProviders` parameter — it's empty by default, so nothing is narrowed until you list them:

```
parameters:
    routeBindingProviders:
        - App\Providers\RouteServiceProvider
```

```
public function handle(Request $request): void
{
    $video = $request->route('video_id'); // Video — no assert() needed
}
```

Scope: only a single constant-string argument is narrowed (`route()` with no argument, a default argument, a dynamic name, or an unknown parameter keeps its default type); only the instance method is covered (the `Request::` facade's static form is out of scope); `Route::bind()` closures without a return-type hint, and `Route::model()` bindings with a missing-model callback, are skipped. A bound parameter is typed as the **non-null** model — matching the intent of the `assert()` calls it replaces. That over-claims by dropping `null` when the current route lacks the parameter (an optional `{param?}` segment, or shared middleware/helpers reached from routes without it), so use it for code reached only via routes that define the parameter and keep an explicit null check where a request may legitimately lack it.

**Implicit bindings.** Parameters bound implicitly (by the controller's type-hint, with no `Route::model()` declaration) are resolved as a fallback by listing your route files in `routeFiles`:

```
parameters:
    routeFiles:
        - routes/web.php
        - routes/api.php
```

It reads each `Route::('uri/{param}', Action)` — an invokable `Controller::class` or a `[Controller::class, 'method']` — and types `route('param')` as the action parameter Laravel would bind to it (matched by name or `Str::snake()`, exactly as the framework does), unioned across every route that declares the parameter. Explicit provider bindings win over implicit ones. It is fail-safe: closures, group-level controllers, `Route::resource`, non-model type-hints, and unreadable files are skipped, leaving the default type — it never mis-types. Paths are resolved from the working directory PHPStan runs in (your project root), so project-relative paths like `routes/web.php` work; the extension parses the files statically — it does not boot your application.

> **Adopting this is a one-time assert-removal sweep.** Once `route('x')` is typed as the bound model, any existing `assert($x instanceof Model)` or `instanceof` guard after it becomes "always true" — PHPStan reports it as a redundant condition. (This is an expression-type resolver, so `treatPhpDocTypesAsCertain: false` does not soften it.) Removing those now-redundant asserts is the point of the feature; expect a one-off cleanup when you first enable `routeBindingProviders`. Keep only the guards on routes that may legitimately lack the parameter (the non-null caveat above).

Parameter closure type extensions
---------------------------------

[](#parameter-closure-type-extensions)

### Relation-existence closure builder typing

[](#relation-existence-closure-builder-typing)

`RelationExistenceClosureBuilderParameterExtension` types the closure passed to Eloquent's relationship-existence methods (`whereHas`, `orWhereHas`, `whereDoesntHave`, `orWhereDoesntHave`, `has`, `orHas`, `doesntHave`, `orDoesntHave`) as the *related* model's builder. Laravel types the callback as `Closure(Builder)`, but when the relation is passed as a string PHPStan cannot bind `TRelatedModel`, so it falls back to `Builder`. Under `checkModelProperties` that makes every `->where(RelatedModel::SOME_COLUMN)` inside the closure fail on valid columns.

```
/** @param Builder $query */
public function scopeWithPublishedPosts(Builder $query): void
{
    // $q is Builder — Post::PUBLISHED resolves instead of erroring against base Model.
    $query->whereHas('posts', fn (Builder $q) => $q->where(Post::STATUS, Post::PUBLISHED));
}
```

It is registered automatically — no configuration. The closure parameter needs no annotation (a bare `Builder` hint is narrowed in place), arrow functions are preserved, dotted nested relations (`'posts.comments'`) resolve to the last related model, and a related model with a custom builder (`HasBuilder`/`newEloquentBuilder()`) keeps that builder type. It fails safe: when the model or relation can't be proven (dynamic relation name, unknown relation), the default typing stands, so a genuinely wrong column on the correct related model still fails.

> **Use `void` closures, not closures that return the builder.** Eloquent's `Builder` is invariant, and Laravel types the callback's return as `mixed` (it's ignored at runtime — the closure constrains the query in place). Once the parameter is narrowed to the related model, a closure that *returns* the builder — e.g. `fn ($q) => $q->where(...)` or `return $q->where(...)` — reports `return.type: should return Builder but returns Builder`. Write the callback as a `void` block instead; the return value serves no purpose:
>
> ```
> // Triggers a return.type error (returns the narrowed builder):
> $query->whereHas('posts', fn (Builder $q) => $q->where(Post::STATUS, Post::PUBLISHED));
>
> // Clean — the closure constrains in place and returns nothing:
> $query->whereHas('posts', function (Builder $q): void {
>     $q->where(Post::STATUS, Post::PUBLISHED);
> });
> ```

Testing
-------

[](#testing)

```
composer test
```

Before opening a PR, run the full pipeline (Pint, Rector, PHPStan, tests):

```
composer qa
```

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for release notes.

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

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md).

Security
--------

[](#security)

Please email  instead of filing a public issue.

Credits
-------

[](#credits)

- [Hihaho](https://github.com/hihaho)
- [All contributors](https://github.com/hihaho/phpstan-rules/contributors)

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

60

—

FairBetter than 98% of packages

Maintenance98

Actively maintained with recent releases

Popularity37

Limited adoption so far

Community18

Small or concentrated contributor base

Maturity72

Established project with proven stability

 Bus Factor1

Top contributor holds 50.7% 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 ~33 days

Recently: every ~0 days

Total

31

Last Release

12d ago

Major Versions

v0.1.0 → v1.0.02024-05-02

v1.1.1 → v2.x-dev2024-11-27

v1.2.0 → v2.0.12024-12-11

v1.2.1 → v2.1.02025-02-26

v2.2.0 → v3.0.02026-04-12

PHP version history (2 changes)v0.1.0PHP ^8.2

v3.0.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/31760627?v=4)[hihaho](/maintainers/hihaho)[@hihaho](https://github.com/hihaho)

---

Top Contributors

[![SanderMuller](https://avatars.githubusercontent.com/u/9074391?v=4)](https://github.com/SanderMuller "SanderMuller (105 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (33 commits)")[![RobertBoes](https://avatars.githubusercontent.com/u/2871897?v=4)](https://github.com/RobertBoes "RobertBoes (32 commits)")[![Treggats](https://avatars.githubusercontent.com/u/27585?v=4)](https://github.com/Treggats "Treggats (21 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (16 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

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

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

###  Alternatives

[larastan/larastan

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

6.5k55.4M8.5k](/packages/larastan-larastan)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k91.9k1](/packages/mike-bronner-laravel-model-caching)[api-platform/laravel

API Platform support for Laravel

58171.6k14](/packages/api-platform-laravel)[calebdw/larastan

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

15118.7k4](/packages/calebdw-larastan)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45444.2k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

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