Help us improve
Share bugs, ideas, or general feedback.
From phpstan-type-trace
Inspects the full type-inference chain for a PHPStan/Larastan error by running the phpstan-trace CLI. Use when the type crosses scopes, involves generics, or source reading fails.
npx claudepluginhub kayw-geek/phpstan-type-trace --plugin phpstan-type-traceHow this skill is triggered — by the user, by Claude, or both
Slash command
/phpstan-type-trace:phpstan-traceThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Inspect the full type-inference chain of a variable at a PHP source location.
Guides PHPStan error resolution prioritizing refactoring over phpDoc and ignoring, with Nette patterns, baseline management, and type tests. Use before running PHPStan or fixing errors.
Guides root cause analysis for PHP bugs using 5 Whys, fault tree analysis, git bisect, and stack trace parsing. Useful for debugging persistent errors.
Provides non-invasive PHP debugging with Xdebug tools: xtrace for execution traces, xstep for breakpoints and stepping, xprofile for performance, xcoverage for test coverage, xback for stack traces.
Share bugs, ideas, or general feedback.
Inspect the full type-inference chain of a variable at a PHP source location.
Invoke this skill before attempting to fix a PHPStan type error when any of these signals are present:
$user->name, $model->created_at), webmozart/assert or beberlei/assert guards, doctrine collections, dynamic-return-type extensions (e.g. Model::query()).array{...} shapes, or template parameters that don't match what you expect from reading the signature.Cannot access property X on string, ... expects Foo, Foo|null given, ... always evaluates to true/false, or a similar "where did this type come from?" message in any of the above contexts.The CLI returns every event (parameter binding, assign, compound-op, narrowing, read) that shaped the variable from function entry up to the failing line — so you can see where the wrong type came in, instead of guessing.
Skip the CLI and just read the source if all of these hold:
?? default, a missing null guard, or a typo in a property name.Also skip for non-type errors: Call to undefined method, Class X not found, Access to undefined constant — these don't need a type-inference chain.
Rule of thumb: if you'd confidently write the fix in under 30 seconds from reading the source, skip the trace. If you'd be guessing, run it.
From the error message, extract:
$user, user->profile, self::$count
are all valid)Call the CLI in JSON mode:
./vendor/bin/phpstan-trace inspect <file>:<line> <var> --json
Quote the variable if it contains > or ; to keep your shell from
misinterpreting it:
./vendor/bin/phpstan-trace inspect src/Service.php:42 'user->profile' --json
Read the returned chain array. Each entry has line, origin (param,
assign, assign-op, assign-ref, array-write, narrow, read), and
the inferred type at that point (PHPStan's full description — including
generics, array shapes, union narrowing, template parameters). Two optional
fields may also appear:
reason — on narrow events, the predicate that justified the narrowing
(is_string($x), $x !== null, $x instanceof Foo, ...).via — third-party PHPStan extensions that shaped the type at this
event. Attached to:
assign / assign-op when the RHS is a call resolved by a
dynamic-return-type extension (e.g.
["NewModelQueryDynamicMethodReturnTypeExtension"]).narrow when an if / ternary condition contains a call resolved by
a type-specifying extension (e.g.
["AssertTypeSpecifyingExtension"] for webmozart Assert).read when a magic / virtual property is owned by a properties
class-reflection extension (e.g. larastan's model attribute
extensions).
Built-ins shipped by phpstan/phpstan core are filtered out; official
add-on packages (e.g. phpstan/phpstan-webmozart-assert) are listed even
though they live under the PHPStan\ namespace.Use the chain to decide the fix:
param: fix the caller, or add a type
guard at function entry.read event shows the type narrowed unexpectedly: an earlier
assignment widened it — fix that assignment.?Foo reached the failing line without an assert/if ($x !== null)
narrowing event: add the null check.via and the inferred type looks wrong or
surprising: the cited extension is the source. Read that extension's
source (or its docs) before assuming a PHPStan bug or rewriting the call.
On a narrow event, via together with the reason (the call signature)
tells you exactly which specifier produced the post-guard type; on a
read event, via names the properties-reflection extension that owns
the magic / virtual attribute.PHPStan error in a Laravel/larastan project:
app/Http/Controllers/ReportController.php:84:
Parameter #1 $value of function number_format expects float, mixed given.
The relevant code:
public function export(Request $request): StreamedResponse
{
$users = User::query()->whereActive()->get();
return response()->streamDownload(function () use ($users, $request) {
foreach ($users as $user) {
$amount = $this->resolveAmount($user, $request->input('period'));
echo number_format($amount, 2); // line 84
}
}, 'report.csv');
}
private function resolveAmount(User $user, mixed $period): float
{
return $user->lifetime_value ?? 0.0;
}
Reading the source: resolveAmount() is typed float. The error says mixed. What?
Three scopes are involved (export → closure → resolveAmount), and lifetime_value is a larastan-typed Eloquent magic attribute. Source-only reading would either miss the cause or take 10 minutes of grep. Run the trace:
./vendor/bin/phpstan-trace inspect app/Http/Controllers/ReportController.php:84 amount --json
Returned chain:
{
"found": true,
"chain": [
{"line": 83, "origin": "assign", "type": "mixed",
"via": ["ModelDynamicMethodReturnTypeExtension"]},
{"line": 84, "origin": "read", "type": "mixed"}
]
}
The via tells you instantly: larastan's ModelDynamicMethodReturnTypeExtension resolved resolveAmount(...) to mixed, not float. Why? Because in this codebase User is a generic stub without a registered Eloquent ide-helper, and larastan widens the return when the receiver type is ambiguous. The fix is not in export() — it's adding a proper @property float $lifetime_value PHPDoc on User, or running php artisan ide-helper:models.
Without the trace you'd cast in export() (wrong fix, masks the root cause) or rewrite resolveAmount() to return mixed (cascades the problem). The chain points directly at the extension responsible.
The CLI runs phpstan internally on the target file with a dump environment
variable set; it does not modify your source or your phpstan config.
A found: false JSON response means the variable is not trackable (e.g.
$arr['key'] array-dim access, dynamic property fetch) or no events occurred
before the queried line. In that case fall back to reading the source.
For interactive (non-agent) use, omit --json for a human-readable chain.
traceType($value) marker calls in source still work for ad-hoc human
debugging — the CLI and the marker share the same collector pipeline.
PHPStan result cache caveat. PHPStan caches analysis per source file. If you change the target file between runs, the chain updates automatically; but if the chain output looks identical to a previous run even though you expect a change (e.g. you edited a callee, a stub, a config, or upgraded the extension), the cache is likely stale. Clear it once and re-run:
./vendor/bin/phpstan clear-result-cache
./vendor/bin/phpstan-trace inspect <file>:<line> <var> --json
Symptom: dump-mode emits zero __TYPETRACE_DUMP__ errors for a file you know
has tracked variables → cache miss. Clear and retry before assuming a bug.