amarucci/phpstan-livewire

Teaches PHPStan's dead code detector to read Livewire components and their Blade templates.

Maintainers

Package info

gitlab.com/aMarucci/phpstan-livewire

Issues

Type:phpstan-extension

pkg:composer/amarucci/phpstan-livewire

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

v0.1.1 2026-09-02 00:09 UTC

This package is auto-updated.

Last update: 2026-09-01 22:11:54 UTC


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:

  1. it is a framework entry point — render, mount, boot, rules, or a per-property hook such as updatedTitle;
  2. it carries an attribute driving it from outside — #[On], #[Url], #[Computed], #[Modelable], #[Reactive], #[Locked], #[Session], #[Js], #[Renderless];
  3. 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\CreatePostlivewire.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

FormExample
wire: directivewire: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 attributex-data="{ s: '<!--' }" is not shielded, unlike <script> blocks.
  • Unresolvable templates — see above: the extension abstains rather than accuse.

License

MIT.