Static analysis is not a replacement for tests. It's a different tool that catches a different class of bug - one that tests often miss entirely. I run Larastan on a multi-tenant Laravel SaaS I work on as freelance lead developer, and it has caught real bugs before they reached production. Here's what it does, how to set it up, and how to ramp up without grinding your team to a halt.
PHPStan and Larastan: What's the Difference?
PHPStan is the underlying static analysis engine for PHP. It reads your code without running it, builds a type model, and reports places where types don't match: a method that might return null that you're treating as a string, a variable that could be an int or a bool depending on a branch you didn't handle, a function argument of the wrong type.
Larastan is a PHPStan extension for Laravel. Laravel leans heavily on magic: Eloquent relationships, model properties inferred from database columns, facades, dynamic request attributes. PHPStan on its own doesn't understand any of that. Larastan teaches it. Without Larastan, running PHPStan on a Laravel codebase produces a wall of false positives that's not worth working through. With it, PHPStan understands User::find() returns User|null, that $request->validated() returns an array, and that your Eloquent relationships exist and have types.
The practical answer: use Larastan, not bare PHPStan, on any Laravel project.
What Static Analysis Catches That Tests Don't
Tests verify behaviour - "given these inputs, I get this output". Static analysis verifies types and control flow across the entire codebase, without you having to write a test that exercises each path. The categories that PHPStan catches and tests typically don't:
- Missing return type handling. A method returns
Model|null. You use the result without a null check. The tests pass because every test fixture includes the model. Production fails when it doesn't. - Dead code. A branch that can never be reached because a type check is always true or always false.
- Wrong argument types. Passing an
intwhere a string is expected, or an array where a single value is expected. PHP's loose typing means these don't always blow up immediately. - Undefined properties and methods. You refactored a class and missed a call site. The tests cover the happy path; the changed code path has no test.
- Unreachable catch blocks. An exception type that the try block can never throw.
None of these require a running app. PHPStan finds them in seconds, on every push.
Setting Up Larastan
Install via Composer:
composer require larastan/larastan --dev Create a phpstan.neon (or phpstan.neon.dist) in your project root:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- app
- config
- database
- routes
level: 5
ignoreErrors: [] Run it:
./vendor/bin/phpstan analyse That's the minimum. Most of the configuration time goes into deciding which level to start at and what to do with the output.
Levels 0-9: How to Ramp Up Without Breaking Everything
PHPStan has ten levels (0 through 9). Level 0 catches the most obvious errors. Level 9 is strict: every type must be known, every nullable must be handled, return types must be declared everywhere. The difference in error count between level 5 and level 9 on a real codebase is often several hundred issues.
The pragmatic approach:
- New projects: start at level 5. It catches the useful bugs without drowning you in pedantry. You can tighten from there once the baseline is green.
- Existing projects: use a baseline file. More on that below.
- Don't aim for level 9 on day one. Going from 0 to max on a live codebase is a multi-day fix that produces a PR nobody wants to review and a high chance of introducing regressions. Pick a level you can get to green on, then raise it incrementally.
The levels that deliver the most bang for your time are roughly 4-6. Below 4 you're catching very obvious problems. Above 7 you're mostly fighting type annotations on third-party code you didn't write. I run level 5 on the SaaS project and it catches meaningful issues regularly.
Baseline Files for Legacy Projects
If you add PHPStan to an existing project at level 5, you'll typically get hundreds of errors on the first run. You can't fix all of them before you commit. The answer is a baseline file: a snapshot of the current errors that PHPStan ignores, so you can enforce "no new errors" without requiring a full historical cleanup first.
Generate it:
./vendor/bin/phpstan analyse --generate-baseline This creates phpstan-baseline.neon. Add it to phpstan.neon:
includes:
- vendor/larastan/larastan/extension.neon
- phpstan-baseline.neon Commit both files. From now on, PHPStan passes in CI. Existing errors are tracked in the baseline. New code must be clean. You work down the baseline over time as you refactor - or you set a sprint goal to clear it entirely and raise the level. Either way, you're not blocked from starting.
Common Findings on Laravel Projects
A few patterns come up consistently when I add Larastan to a Laravel project.
Eloquent magic and undefined properties. A model property accessed as $user->profile_photo when that accessor doesn't exist or was renamed. Larastan understands Eloquent's property system, so it catches this. You can help it further by adding PHPDoc blocks to your models - tools like ide-helper generate these from your database schema automatically.
Request attribute access. $request->user_id or $request->get('user_id') returns mixed. Code that treats it as an int without casting or validating will get flagged. The fix is usually to use $request->integer('user_id'), $request->validated(), or an explicit cast - all of which are better practice anyway.
Missing null checks after find(). User::find($id) returns User|null. If you call a method on the result without checking for null, PHPStan will tell you. The fix is findOrFail() where you want an exception, or an explicit null check where you don't.
Incorrect return types. A method that always returns a Collection but is typed as returning array, or vice versa. Not a runtime error, but it confuses everything downstream - IDE autocomplete, other static analysis rules, and future developers.
Useful Extensions
Beyond Larastan itself, two extensions are worth adding.
ShipMonk PHPStan rules adds a set of opinionated rules on top of standard PHPStan: enforcing that you handle all cases in a match expression, banning certain loose comparisons, requiring that thrown exceptions extend the right base class. I use these on the SaaS project alongside Larastan. They push you toward more explicit, intentional code - not always comfortable, but consistently useful. Install and add to your phpstan.neon includes the same way as Larastan.
Mockery rules (phpstan-mockery) are worth adding if your test suite uses Mockery. They teach PHPStan about Mockery's proxy methods so mock assertions don't generate false positives.
CI Integration
PHPStan is fast enough to run on every push. In GitHub Actions:
- name: Static analysis
run: ./vendor/bin/phpstan analyse --no-progress --error-format=github The --error-format=github flag formats errors as GitHub annotations, so they appear inline on the diff in the PR. That makes it far more likely developers will read and fix them rather than scrolling past a wall of terminal output.
Add a composer step to cache the vendor directory between runs and PHPStan's own result cache will further cut run time on large codebases.
For more on testing and quality tooling for Laravel, see Laravel best practices for startups and scaling a Laravel backend.
The Real Benefit
The thing I notice most after running Larastan for a while is not the bugs it catches on day one. It's the discipline it enforces going forward. When every new method needs a return type, when nullable returns have to be handled explicitly, when you can't quietly pass the wrong type and have PHP coerce it - the code gets more intentional. Refactors become safer because PHPStan tells you immediately if you've broken a call site you forgot about.
On the SaaS project I work on, running Larastan ^3.9 with ShipMonk rules at level 5 has made me more confident pushing changes to a large multi-tenant codebase. It's not a substitute for tests - it's something tests can't be. The two tools cover different ground and both pay for themselves.
I'm a freelance Laravel developer based in Wiltshire, near Bath, working with startups and scale-ups across Bath, Bristol, and the UK. If you want to add static analysis to an existing Laravel project, or you're building something new and want it set up right from the start, reach out.