PHPackages                             boring-o11y/wirestan - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. boring-o11y/wirestan

ActivePhpstan-extension[Security](/categories/security)

boring-o11y/wirestan
====================

PHPStan rule for Livewire components: public properties that are never reassigned outside lifecycle methods must be marked #\[Locked\], so the client cannot tamper with them via $wire.set.

v0.1.0(yesterday)04↑2900%MITPHP ^8.2

Since Jul 22Compare

[ Source](https://github.com/boring-o11y/wirestan)[ Packagist](https://packagist.org/packages/boring-o11y/wirestan)[ Docs](https://github.com/boring-o11y/wirestan)[ RSS](/packages/boring-o11y-wirestan/feed)WikiDiscussions Synced today

READMEChangelogDependencies (3)Versions (2)Used By (0)

wirestan
========

[](#wirestan)

[![Latest Version](https://camo.githubusercontent.com/d285a815951f83888fb4a095fb13c0cd782ab24eac78c6f5a73266e95d8c4354/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f626f72696e672d6f3131792f776972657374616e2e737667)](https://packagist.org/packages/boring-o11y/wirestan)[![License](https://camo.githubusercontent.com/02dfaef57746d423e46be34c48ac6f4ef39c85f3438de989148caba009fc014c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f626f72696e672d6f3131792f776972657374616e2e737667)](LICENSE)

PHPStan / [Larastan](https://github.com/larastan/larastan) rules for **Livewire components**.

Every public property on a Livewire component is part of the client-editable state: the browser can set any of them with `$wire.set('prop', value)`, and Livewire will happily hydrate the new value on the next request. That is exactly what you want for a `wire:model`-bound input — and exactly what you *don't* want for `$bookingId`, `$tenantId`, `$role`, or anything else the server seeded in `mount()` and then trusted.

Livewire's answer is the `#[Locked]` attribute, which makes the property server-only. The failure mode of forgetting it is silent: nothing throws, the component works in the browser, and the IDOR only shows up when someone goes looking for it.

`wirestan` catches the omission in CI.

Requirements
------------

[](#requirements)

- PHP `^8.2`
- PHPStan `^2.0` (works great alongside [Larastan](https://github.com/larastan/larastan))
- Livewire 3 or 4 in the analysed project

Installation
------------

[](#installation)

```
composer require --dev boring-o11y/wirestan
```

### Registering the rules

[](#registering-the-rules)

If you use [`phpstan/extension-installer`](https://github.com/phpstan/phpstan-extension-installer)(recommended), there is **nothing else to do** — the rule registers automatically.

Otherwise, include the bundled `extension.neon` from your `phpstan.neon`(or `phpstan.neon.dist`):

```
includes:
    - vendor/boring-o11y/wirestan/extension.neon
```

That's it — the rule now runs as part of your normal analysis:

```
vendor/bin/phpstan analyse
```

Rules
-----

[](#rules)

RuleIdentifierIn one line[LockedPublicPropertyRule](#lockedpublicpropertyrule)`boringO11yWirestan.lockedPublicProperty`Never-reassigned public properties must be `#[Locked]`### `LockedPublicPropertyRule`

[](#lockedpublicpropertyrule)

Reports public properties of `Livewire\Component` subclasses that are never reassigned outside the lifecycle *seed* methods and are missing `#[Livewire\Attributes\Locked]`.

```
use Livewire\Component;

class ShowBooking extends Component
{
    public int $bookingId = 0;   // ← flagged

    public string $note = '';    // fine: reassigned by saveNote()

    public function mount(int $bookingId): void
    {
        $this->bookingId = $bookingId;
    }

    public function saveNote(string $note): void
    {
        $this->note = $note;
    }
}
```

```
Livewire public property ShowBooking::$bookingId is never reassigned outside
lifecycle methods. It must be marked #[Livewire\Attributes\Locked] — otherwise
the client can mutate it via $wire.set.

```

The fix is one attribute:

```
use Livewire\Attributes\Locked;

#[Locked]
public int $bookingId = 0;
```

#### What counts as a mutation

[](#what-counts-as-a-mutation)

Writes inside the **seed methods** — `mount`, `__construct`, `boot`, `booted`, `hydrate`, `dehydrate` — are server-controlled initialisation, not user-driven mutation, so they do *not* exempt a property. A write anywhere else does.

Recognised as a mutation: plain assignment, compound assignment (`.=`, `+=`, …), reference assignment, `++`/`--`, array-element writes (`$this->rows[] = …`), list destructuring (`[$this->a, $this->b] = …`), and `$this->reset('name')`with literal property names.

#### What the rule deliberately skips

[](#what-the-rule-deliberately-skips)

- **Properties already marked `#[Locked]`**, obviously.
- **`static` and `readonly` properties** — not client-editable state.
- **`Livewire\Form` properties.** A Form is mutated through nested fields (`wire:model="form.email"`) and never reassigned wholesale, so it reads as immutable — but locking the root breaks every nested update at runtime with `CannotUpdateLockedPropertyException`. Livewire de/hydrates Forms natively, so they are not a `$wire.set` vector.
- **Framework-reserved properties**: `$listeners`, `$rules`, `$messages`, `$validationAttributes`, `$queryString`, `$paginationTheme`. Configurable, see below.
- **The entire component**, when a non-seed method performs an *opaque mass-assignment* that could touch any public property: `$this->fill()`, `fillData()`, `mergeData()`, `resetExcept()`, a no-argument or non-literal-argument `$this->reset()`, or a dynamic write `$this->{$name} = …`. Immutability can no longer be proven statically, so the rule stays quiet rather than emit false positives.

#### Configuration

[](#configuration)

Override the reserved-property list from your `phpstan.neon`:

```
parameters:
    wirestan:
        reservedProperties:
            - listeners
            - rules
            - messages
            - validationAttributes
            - queryString
            - paginationTheme
            - myOwnConvention
```

Adopting on an existing codebase
--------------------------------

[](#adopting-on-an-existing-codebase)

The rule will light up on any Livewire codebase that predates it. Generate a [baseline](https://phpstan.org/user-guide/baseline) so it only fails on *new*violations, then burn the entries down:

```
vendor/bin/phpstan analyse --generate-baseline
```

Each baselined entry is a real decision: add `#[Locked]` where the property is server-controlled, or confirm it is genuinely a `wire:model`-bound input and remove it from the baseline once the rule stops matching.

To silence an individual finding, use the error identifier:

```
parameters:
    ignoreErrors:
        -
            identifier: boringO11yWirestan.lockedPublicProperty
            path: app/Livewire/SomeComponent.php
```

Development
-----------

[](#development)

```
composer install
composer test      # phpunit
composer phpstan   # analyse the rule itself
```

Tests use PHPStan's `RuleTestCase` against fixtures in `tests/Fixtures`, with minimal `Livewire\*` stubs in `tests/Stubs` so the package has no runtime dependency on Livewire itself.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance100

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/753de6df816ebfae6db4fc80362629f3bd6f5205059bd1ee9f1478b75947fdc3?d=identicon)[boring-o11y](/maintainers/boring-o11y)

---

Tags

PHPStanlaravelstatic analysissecurityphpstan-extensionlivewirelarastanphpstan-rules

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/boring-o11y-wirestan/health.svg)

```
[![Health](https://phpackages.com/badges/boring-o11y-wirestan/health.svg)](https://phpackages.com/packages/boring-o11y-wirestan)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laraveldaily/filacheck

Static analysis for Filament projects - detect deprecated patterns and code issues

11975.6k](/packages/laraveldaily-filacheck)[calebdw/larastan-livewire

A Larastan / PHPStan extension for Livewire.

47698.4k6](/packages/calebdw-larastan-livewire)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
