amarucci / phpstan-livewire
Teaches PHPStan's dead code detector to read Livewire components and their Blade templates.
Package info
gitlab.com/aMarucci/phpstan-livewire
Type:phpstan-extension
pkg:composer/amarucci/phpstan-livewire
Requires
- php: ^8.4
- livewire/livewire: ^4.0
- phpstan/phpstan: ^2.0
- shipmonk/dead-code-detector: ^1.4
Requires (Dev)
- laravel/framework: ^13.0
- laravel/pint: ^1.27
- pestphp/pest: ^5.0
- pestphp/pest-plugin-phpstan: ^5.0
README
Teaches shipmonk/dead-code-detector to read Livewire components.
The problem
A Livewire component has almost no visible link to the rest of the PHP code:
class Counter extends Component
{
public int $count = 0; // read by the Blade template
public function increment(): void // called from the browser payload
{
$this->count++;
}
}
The dead code detector sees none of that and reports all three members as
unused. Its built-in BladeUsageProvider only follows the data passed to
view('tpl', [...]); it never reads Blade sources.
Blanket-sparing every public member silences the noise but creates a total blind
spot: deleting the button that called resetCount() no longer produces any
signal.
How members are judged
A public member of a Livewire component is considered used when one of these holds:
- it is a framework entry point —
render,mount,boot,rules, or a per-property hook such asupdatedTitle; - it carries an attribute driving it from outside —
#[On],#[Url],#[Computed],#[Modelable],#[Reactive],#[Locked],#[Session],#[Js],#[Renderless]; - its own component's templates name it.
Otherwise it is reported as dead.
Per-component templates
Templates are resolved from the view('…') literals found in the component
class and from the conventional name
(App\Livewire\Posts\CreatePost → livewire.posts.create-post).
Pooling names from every template in the project would be simpler but useless:
Livewire action names repeat heavily — save, delete, close, submit — and
a single wire:click="save" anywhere would spare every save() in the
codebase.
Resolution follows what a template pulls in, transitively: @include,
@includeIf, @includeWhen, @includeUnless, @includeFirst, @each, and
anonymous Blade components such as <x-form.field />. A partial shares the
scope of the view including it, so skipping them would report live members as
dead.
Recognised reference forms
| Form | Example |
|---|---|
wire: directive | wire:click="increment", wire:model.live="title" |
| Alpine | $wire.increment() |
| JavaScript | @this.increment() |
| computed property | {{ $this->fullName }} |
| Blade variable | {{ $count }}, @if ($count > 0) |
Blade {{-- … --}} and HTML <!-- … --> comments are stripped first: what they
mention is dead. <script> blocks are set aside beforehand, otherwise a
const marker = "<!--"; would swallow the code following it.
When the extension abstains
A component whose templates cannot be resolved — a dynamically chosen view with no readable literal — has all its public members spared. Staying silent when nothing can be asserted is the only acceptable behaviour; the opposite would have people delete live code.
Installation
composer require --dev amarucci/phpstan-livewire
With phpstan/extension-installer
you are done. Otherwise include it manually:
includes:
- vendor/amarucci/phpstan-livewire/extension.neon
Configuration
One setting, whose default suits a standard Laravel application:
parameters:
livewireDeadCode:
viewPath: %currentWorkingDirectory%/resources/views
Result cache
The extension registers a
ResultCacheMetaExtension
hashing the path and content of every template.
Without it PHPStan would only invalidate on PHP, config or composer.lock
changes, so removing a {{ $count }} from a template would leave the previous
verdict in place. Paths are hashed as well as contents, because renaming a
template changes which members a component can address.
Note that any template change invalidates the whole cache and triggers a full re-analysis. That is the cost of correctness.
Known limitations
<!--inside an HTML attribute —x-data="{ s: '<!--' }"is not shielded, unlike<script>blocks.- Unresolvable templates — see above: the extension abstains rather than accuse.
License
MIT.