Every public property on a Livewire component can be set from the browser. Anyone with the devtools open can
run $wire.set('bookingId', 42), and Livewire will hydrate the new value on the next request.
For a property bound to an input with wire:model, that is the point. For
$bookingId, $tenantId or $role, seeded in mount() and trusted
from then on, it is an insecure direct object reference. Livewire's fix is the #[Locked] attribute.
Forgetting it fails silently: nothing throws, the component works, and the hole stays open until somebody goes
looking.
The rule
wirestan adds a PHPStan rule that flags a public property on a Livewire component when nothing but the
lifecycle methods ever assigns it and it is not marked #[Locked].
class ShowBooking extends Component
{
public int $bookingId = 0; // flagged: only mount() sets it
public string $note = ''; // fine: saveNote() changes it
public function mount(int $bookingId): void
{
$this->bookingId = $bookingId;
}
public function saveNote(string $note): void
{
$this->note = $note;
}
}
The fix is the one attribute, #[Locked], on $bookingId. With
phpstan/extension-installer the rule registers itself; otherwise it is one line in
phpstan.neon. It runs alongside Larastan.
Built to stay quiet when it cannot be sure
Writes in mount(), boot(), hydrate() and the other seed methods do not
count as changes, because the server makes them. Any other write does, including +=,
array appends and $this->reset('name').
Livewire Form objects are skipped, because locking one breaks every nested wire:model. So are
static and readonly properties and Livewire's reserved ones, like $listeners and
$rules. When a component uses $this->fill() or a dynamic property write, which could
change any property, the rule skips the whole component rather than guess.
On a codebase that predates it
Expect it to find things. Generate a PHPStan baseline so CI only fails on new violations, then go through the
baselined entries one at a time: each is either a property that needs #[Locked] or an input you can
confirm is meant to be editable.
Every configuration key, and how to run the test suite, is in the README on GitHub. Bugs and questions go in its issues.