canvural/phpstan-blade-rule
A PHPStan rule for Laravel Blade templates that brings static analysis to your views. Catch undefined variables, type mismatches, and other errors in .blade.php files before runtime by analyzing compiled templates with PHPStan.
canvural/phpstan-blade-rule adds a PHPStan rule that statically analyses .blade.php files and reports variables that are used in a template but never defined, passed in, or introduced by a Blade directive. It is the Blade equivalent of "undefined variable" checks in plain PHP — the class of bug that normally only surfaces at runtime, in production, on the one page nobody clicked during QA.
Install and wire it up (3 steps):
composer require --dev canvural/phpstan-blade-rule
# phpstan.neon
includes:
- vendor/canvural/phpstan-blade-rule/extension.neon
parameters:
blade:
paths:
- resources/views
vendor/bin/phpstan analyse
Check the package's extension.neon and README for the exact parameter keys in your installed version — the config surface is small but has changed between releases, and the include file is the authoritative source.
Where to look first: run it once against resources/views with no baseline and read the output. The first errors you see will almost always be one of three things: a typo in a variable name ($artcle instead of $article), a partial that expects data its caller never passes, or a variable injected by a view composer that static analysis genuinely cannot see. That last category is the one you need to make a policy decision about on day one.
First real use case: find every @include('partials.header') where partials/header.blade.php references $title but the include call passes no second argument. Those are latent Undefined variable errors waiting for a user to hit the page.
Run it as a separate analysis pass. Blade analysis has different failure modes and a different noise profile than your PHP analysis. Keeping it in its own config file (e.g. phpstan-blade.neon) lets you run it in a dedicated CI job, tune its level independently, and avoid slowing down the main PHPStan run on large codebases.
vendor/bin/phpstan analyse -c phpstan-blade.neon --memory-limit=1G
Adopt incrementally with a baseline. On an existing project the first run will produce a wall of errors, most of them legitimate but none of them urgent. Generate a baseline, commit it, and let the rule protect new views from day one while the backlog is burned down opportunistically.
vendor/bin/phpstan analyse -c phpstan-blade.neon --generate-baseline
Pair it with Larastan. Larastan teaches PHPStan about the Laravel container, facades, and Eloquent; this package teaches it about Blade. They are complementary, not overlapping, and both belong in the includes list. If you only install one, install Larastan first — this rule is the second-order win.
Make include data explicit. The single biggest behavioural improvement for day-to-day use is passing data to partials as an array rather than relying on the parent's scope:
{{-- fragile: relies on ambient scope --}}
@include('partials.card')
{{-- explicit: analysable, and the intent is obvious --}}
@include('partials.card', ['article' => $article, 'showMeta' => true])
Explicit data arrays are what the analyser can actually verify. They also make partials safely reusable, and they are the pattern v0.4's scoping fix is designed to support.
Treat the rule as a review tool, not a gate. Run it in CI as a non-blocking job for the first few weeks and watch which errors are real. Blade analysis is heuristic; a rule that is 90% precise is extremely useful on a PR diff and extremely annoying as a hard merge gate until you have triaged the false-positive sources (see below).
Use it in the loop on refactors. The highest-value moment for this rule is not CI — it is when you rename a variable in a controller or rename a partial. Running the analysis locally immediately after the change tells you every view that broke, before you open a browser.
@include variable scoping — the v0.4 fix. Prior to v0.4, variables passed to an included template via the second argument were not reliably recognised as in-scope inside the included file, which produced false positives on correctly written partials. If you are on v0.3 and you suppressed those errors with ignores or baselines, upgrade to v0.4 and remove the suppressions — otherwise you are carrying dead config that will mask real regressions later. This is a fix, not a breaking change, but it does change your error output, so expect baseline churn on upgrade.
Dynamic includes are invisible to static analysis. @include($partialName), @includeIf(...) with a computed name, @each(...), and @includeFirst(...) cannot be resolved to a file, so nothing inside the target is verified and nothing it expects is checked. Any template reached only through a dynamic path is effectively unanalysed. Audit for these if your error coverage looks suspiciously clean.
View composers and View::share are false-positive generators. Variables injected at runtime by a service provider are invisible to the analyser. Do not chase these with increasingly clever ignore patterns — pick one policy and apply it consistently:
$currentUser, $cartCount), add a narrowly scoped ignoreErrors entry with a comment explaining the source.@php /** @var ... */ annotations as a blanket silencer; they are useful for a single ambiguous variable, corrosive when used everywhere.Blade-provided variables need to be accounted for. $loop inside @foreach, $slot and $attributes inside components, $errors from the share list, and $message in mail views are all legitimate. Verify your installed version handles the ones you rely on before filing bugs; where it does not, these belong in the ignore list, not in a refactor.
Only source .blade.php files are analysed. Compiled views in storage/framework/views are irrelevant and should never be in paths. Likewise, keep the paths list tight — pointing at the whole project root will pull in vendor view files from packages and bury your own errors.
Cache behaviour. PHPStan caches results keyed on analysed file contents, so editing a .blade.php file does invalidate its cached result. But if you change only the include graph (e.g. a partial now receives different data), the included file itself has not changed — if a stale result looks wrong, --clear-result-cache before you start debugging the rule.
Extension point. The package parses Blade into an AST and runs rules against it. If you have a house convention the package does not check (forbidden directives, missing @csrf in forms, disallowed {!! !!} usage in a hardened codebase), writing a custom rule against that AST is far cheaper than a regex-based linter and is the natural place to grow your own view-level guardrails. Read the existing rule's source as the template — it is short.
Pin the version in composer.json if you baseline aggressively. The analyser's precision improves between minor releases, and each improvement shifts error output. A floating constraint plus a large baseline means an unrelated composer update can silently break CI.
How can I help you explore Laravel packages today?