nunomaduro/laravel-code-analyse
Laravel Code Analyse brings static code analysis to your Laravel apps. Run the artisan command to scan your codebase against configurable checks, catch common issues early, and keep your code clean, consistent, and production-ready.
Larastan is a require-dev dependency plus a CI invocation — there is nothing to register, migrate, or deploy.
Minimum viable setup (three steps):
composer require --dev larastan/larastan
# phpstan.neon
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 5
paths:
- app/
vendor/bin/phpstan analyse --memory-limit=2G
Two setup rules worth internalizing immediately:
^2.3 within the 2.x series. Bump both in one PR and read the lockfile diff; a split upgrade leaves the extension resolving against an unsupported PHPStan and produces a broken intermediate state that is worse than not upgrading.Where to look first:
vendor/larastan/larastan/extension.neon — this is the source of truth for services, parameters, and defaults. Read it before copying config snippets from blog posts.docs/custom-types.md in the package — where relation-of, model generics, and builder types are documented.First use case: run it against app/Models/ only, and look at what it says about with(), scopes, and relationship return types. That single directory is where the inference is strongest and where the payoff is most visible in a first afternoon.
Then commit a baseline so the gate is green on day one, and ratchet from there:
vendor/bin/phpstan analyse --generate-baseline
Ladder the level per directory, not globally. PHPStan has no per-path level, so use one config per band and run them as separate CI steps:
phpstan.neon # level 6, app/
phpstan-tests.neon # level 4, tests/
This lets the high-churn, high-value code (app/) sit at a stricter level than test scaffolding without one blocking the other.
Baseline as a ledger, not a mute button. Generate the baseline once, then treat every entry as an IOU with a documented reason. Regenerate only after triage — never before — so the file records decisions instead of masking them. Shrink it deliberately as debt is paid; a baseline that only grows is a gate that verifies nothing.
Annotation hotspots, in priority order. The signatures that pay for themselves fastest:
@return EloquentBuilder<Model> and, new in 3.13, @return relation-of<Model, Related> where a relationship is returned.@return Collection<int, Post> kills the "no value type specified in iterable" class of noise wholesale.app(Foo::class) and resolve(Foo::class) infer correctly when the argument is a class-string; passing a string variable defeats it.Use relation-of<Model, Relationship> at the boundary. The highest-value place is a method signature a human reads — a repository method, a scope, a service returning a loaded relationship. It documents intent and gives the analyzer a concrete type in one line. Don't spray it everywhere; the analyzer infers relationships inside with(), withOnly(), load(), and loadMissing() arrays automatically now, so reach for the explicit type when the value crosses a method boundary.
CI shape. Run it as a blocking job parallel to tests, with caching:
vendor/bin/phpstan analyse --no-progress --error-format=github --memory-limit=2G
--error-format=github gives inline PR annotations, which is the difference between a gate people fix and a gate people re-run until it passes. Keep a second, non-blocking job for anything experimental (bleeding-edge inference, a new rule) so you can evaluate it against real output before it can fail a PR.
Fast local loop. Run the full analysis before pushing, and scope to the file you're editing while working. Pair it with \PHPStan\dumpType($expr) dropped temporarily into a reproducer to see exactly what the analyzer inferred — this converts "why is it complaining?" into a one-minute answer instead of a thirty-minute guess.
Custom rules for project conventions. Larastan covers Laravel semantics; it does not know your house rules (no env() outside config, no DB:: in controllers). Those belong in src/Rules/ of your own small extension, registered in the same phpstan.neon. Start with one rule you already enforce in code review — that's the one with a demonstrated cost.
The 3.13 upgrade will produce a nonzero delta on unchanged code. New relationship inference and stricter collection key types legitimately surface previously silent issues. Capture the pre-upgrade run, upgrade, diff, then triage into three buckets: real bugs, false positives to baseline with a reason, and pre-existing debt for the backlog. Do not regenerate the baseline first.
with(['posts:id' => $callback]) is now reported, and will likely be your largest source of new failures. Column selection combined with a callback is a real correctness and performance pitfall — one of the two is redundant at best. Fix by dropping the column constraint or the callback, and let this one block.
Don't enable PHPStan 2.3's bleeding-edge inference in the blocking gate. Evaluate it in an advisory job first. Opt-in inference for empty collections and closures is high-value but the failure mode is a sudden wave of findings across code nobody is touching that week.
Bootstrap failures in stripped CI environments. The analyzer needs a bootstrapped application — composer install must have run so the package manifest exists. Recent releases fixed a null package-manifest path crash, but the surrounding class of failure (missing .env, missing app key, service providers that reach for the network) still bites. Use a dedicated phpstan-bootstrap.php and set APP_ENV=testing rather than relying on the pipeline's ambient environment.
Exclude generated and IDE-helper files from paths, but don't exclude migrations from the scan. Larastan reads migrations and casts to infer model properties; excluding them from analysis is correct and expected, but the schema scan still needs to find them. If model property inference looks wrong, check squashed_migrations_path and the migration scan settings before adding @property docblocks.
Validate custom query builders explicitly. If you override toBase() or return a custom builder from a model, exercise those classes directly — forwarded-call resolution through custom builders has been an active fix area, and it's exactly the kind of code that lives in one file and is never analyzed on its own.
Wrapper vs. engine: pick one layer deliberately. The zero-configuration wrapper (one command, curated strict defaults, Pint/Rector/GitHub Actions glue) is an accelerator; larastan/larastan is the durable dependency. The moment you need a custom rule, a targeted suppression, or a level change, you are writing PHPStan config anyway. If the wrapper's defaults conflict with team policy, reconcile them into your own phpstan.neon rather than layering — two competing profiles is how a gate becomes decorative. Note also that a wrapper pinned to an older engine lags the inference fixes above; check its release cadence against the engine's.
Prefer inline @phpstan-ignore with an identifier for new code, baseline for legacy. An inline ignore sits next to the thing it excuses and gets noticed on the next edit. A baseline entry is invisible and rots. Reserve the baseline for code you have decided not to touch.
Memory and parallelism. Large Eloquent-heavy apps need --memory-limit=2G or more; CI containers OOM silently and report as a truncated run. Cache the analysis directory across runs. If a run hangs, --debug shows the file being analyzed and -vvv usually identifies the offending expression.
It is not a test suite, a security scanner, or a performance profiler. It changes nothing at runtime, so it is not an input to availability or incident planning. Sell it as what it is: a cheap way to catch the relationship, eager-loading, and collection-key defects that reach production because nothing else looks at them.
How can I help you explore Laravel packages today?