Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpstan Rules Laravel Package

esfahanahan/phpstan-rules

A collection of custom PHPStan rules for Laravel and PHP projects, helping teams catch architectural and coding issues early. Drop it into your phpstan.neon config to enforce stricter static analysis and keep code quality consistent across the codebase.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install via Composer (composer require --dev hosni/<package>), then publish the config if the package ships one (php artisan vendor:publish --tag=<package>-config). The detector is a dev-only dependency — keep it out of production autoload.
  • Look first at the package's artisan entry point (typically php artisan <package>:scan or a listener that runs during APP_DEBUG). Running it once against an existing app is the fastest way to see how noisy it is on your codebase before wiring it into anything.
  • v1.1.0 adds a new rule, so re-run the scanner after upgrading even if you had a clean baseline — you will likely get fresh hits. The new rule reports load() calls that reload a parent relation which was already constrained (e.g. you did $post->comments()->where('approved', true)->get() earlier, then later call $post->load('comments'), silently throwing away the constraint).
  • First use case: take one controller or Livewire component with a known-tricky detail page, run the scanner against just that request/route, and read the report. The load()-reload finding is usually the highest-signal one because it changes query results, not just query count — it is a correctness bug, not just a performance smell.

Implementation Patterns

Local feedback loop. Run the scanner as part of your normal test suite rather than as an ad-hoc command. If the package exposes a middleware or event listener, enable it in phpunit.xml (APP_DEBUG=true, plus whatever env flag the package reads) so every feature test doubles as a detection pass. This is where the new v1.1.0 rule pays off: integration tests that render a page after pre-constraining a relation will surface the reload immediately.

Route-scoped scanning. Rather than scanning the whole app and drowning in legacy findings, drive the scanner from a small set of "canary" routes — the ones with the most relationships. Add routes as you clean them up.

CI gate with a baseline file. Adopt whatever ignore/baseline mechanism the package offers (config array of ignored call sites, or an inline suppression comment). Generate the baseline on the commit where you upgrade to v1.1.0 so the new load() findings for new code fail the build, while existing ones are triaged separately. Never regenerate the baseline silently in CI — diff it in review.

Pair with eager-loading conventions. The load()-reload rule complements, not replaces, standard N+1 detection. A useful workflow:

  1. Detector flags the reload.
  2. Replace $post->load('comments') with $post->loadMissing('comments'), or move the constraint into the eager load ($post->load(['comments' => fn ($q) => $q->where('approved', true)])).
  3. Delete the now-redundant earlier constrained query if it was only there to hydrate the relation.

Assert on results, not just counts. For the new rule specifically, a query-count assertion (DB::listen + counter) will not catch it — the bug is a wrong result set with a similar query count. Pair the detector with a feature test that asserts on the collection contents, otherwise you can "fix" the finding by removing the constraint and stay green.

Per-model opt-out for deliberate reloads. Keep a short, commented config block listing the handful of places where reloading a constrained relation is intentional (e.g. swapping a filtered view for the full set on the same request). Anything not on that list should be treated as a defect.

Gotchas and Tips

  • loadMissing() is not a drop-in fix for every hit. It suppresses the reload when the relation is already loaded — including when it was loaded with a constraint you now want gone. If you genuinely need the unconstrained set, you must unsetRelation('comments') (or use load() deliberately) and should suppress the finding with a comment.
  • Closure-constrained eager loads are the common false negative. load(['comments' => fn ($q) => ...]) counts as constrained, so the rule generally won't fire — but the constraint has to actually be on the relation being loaded. Watch for constraints applied via a scoped relation method ($post->approvedComments()) where the name differs from the relation being load()ed; the detector may not correlate them, so the reload goes unreported. Verify manually in that pattern.
  • Nested relations hide the parent. $post->load('comments.author') reloads comments too. The finding is usually reported against the outer segment; don't "fix" only the nested part.
  • Upgrade ordering matters. If you upgrade to v1.1.0 mid-sprint, run the scanner before and after on the same branch and diff — otherwise new findings look like your own regressions.
  • Debugging a surprising finding: log the actual SQL with DB::listen() around the flagged line. The detector reports the call site; the SQL tells you which constraint was dropped, which is often the fastest way to confirm whether it's a real bug or a false positive worth suppressing.
  • Config quirks: detection rules are usually toggled individually. After upgrading, confirm the new rule is actually enabled in your published config — a stale published config from an earlier version will not include the new key, and the rule may default to off. Republish or diff the config on every minor bump.
  • Extension point: if the package exposes rule registration (a detector collection or a Rule interface), wrap your own checks for house conventions — e.g. "no load() inside a loop" — in the same pipeline so all query-shape findings land in one report and one CI gate.
  • Not a silver bullet. This release improves correctness detection for one specific pattern. Keep the human review habit of asking "who already constrained this relation?" any time you write load().
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource