In a Livewire component, which methods can the browser call, and why must their arguments be treated as untrusted input?
answer
- every public method is an endpoint
- render() and lifecycle hooks are blocked
- #[Computed] methods refuse direct calls
- protected or private for internal helpers
- MethodNotFoundException, a bare 419 in production
basics
~20 sThe browser can call any public, non-static method your Livewire component class declares, referenced in a template or not, except render(), lifecycle hooks and #[Computed] methods. Arguments arrive in an editable JSON request, so authorize and validate them.
solid answer
~30 sLivewire turns every public method declared on your component class, not on the base `Livewire\Component`, into an action: the browser posts a method name and parameters to the update endpoint and Livewire calls it. `wire:click="approve({{ $payslip->id }})"` is only a suggestion; anyone can change the id in devtools, or call a public method no button shows. Livewire refuses `render()`, lifecycle hooks such as `mount()` and `updated*()`, and `#[Computed]` methods, but public methods pulled in from traits you use are reachable. So internal helpers go `protected` or `private`, and every action authorizes and validates its own arguments before it touches the database.
code
php · 21 lines<?php
use App\Models\Payslip;
use Livewire\Component;
class PayslipList extends Component
{
public function approve(int $payslipId): void
{
$payslip = Payslip::findOrFail($payslipId);
$this->authorize('approve', $payslip); // runs before any write
$this->markApproved($payslip);
}
protected function markApproved(Payslip $payslip): void
{
$payslip->update(['approved_at' => now()]);
}
}go deeper
Remember one rule: every public method on a Livewire component is callable from the browser with any arguments, so helpers belong in protected or private methods.
Explain the call path: method name and params in the update payload, the public-methods allowlist, and which names Livewire blocks, such as render, lifecycle hooks and computed methods.
Show how you audit a component: list every public method including trait methods, confirm each authorizes its own arguments, and catch refactors that split the check from the write.
Frame visibility as the component's API surface and argue for team conventions, such as review checklists or static rules, that treat each public Livewire method like a controller route.
## What "public" means in a Livewire component A **Livewire component** is a PHP class, or the class part of a single-file component in Livewire 4, whose state lives in the browser between requests. When a user clicks a button bound with `wire:click`, the browser sends a request to Livewire's single **update endpoint**. The payload carries the component's signed snapshot, any property updates, and a list of **calls**: each is just a method name and an array of parameters. On the server, Livewire rebuilds the component and, for each call, checks the method name against the list of **public, non-static methods declared by your subclass**. Methods declared on `Livewire\Component` itself (and the traits it uses) are excluded; methods on your class, or on traits your class uses, are included. If the name is on the list, Livewire invokes it with the parameters exactly as the browser sent them. Two consequences follow: - A method is callable whether or not any template references it. Hiding the button hides nothing. - Parameters are whatever the client typed. `wire:click="approve(42)"` rendered by your Blade view can be replayed as `approve(43)` with no special tooling. ## What Livewire refuses to call | Method | What happens if the browser names it | |---|---| | `protected` / `private` method | Not on the list: `MethodNotFoundException` ("Public method [x] not found") | | `render()` | Removed from the callable list: same exception | | `mount()`, `boot()`, `hydrate*()`, `updated*()`, `rendering()` and the other hooks | `DirectlyCallingLifecycleHooksNotAllowedException` | | a `#[Computed]` method | `CannotCallComputedDirectlyException` | | a method from a trait your component uses | **Callable**, if it is public | Several of these exceptions, `MethodNotFoundException` among them, render as an empty **419** response when `app.debug` is false and as a full error page in debug mode. ## The payroll example On a payroll approval screen, a list shows each payslip with an Approve button: ```php public function approve(int $payslipId) { // INSECURE: any id the browser sends is approved Payslip::findOrFail($payslipId)->update(['approved_at' => now()]); } ``` A clerk who can see only their own department can call `approve` with another department's id. The fix belongs inside the action: load the record, authorize it against the current user, then act. Livewire offers the `#[Authorize]` attribute or the `authorize()` helper the base component inherits for that check; which rule to write is your policy's business. A subtler leak is the refactor: ```php public function approve(int $id) { $this->authorizeApproval($id); $this->markApproved($id); } public function markApproved(int $id) { /* no check */ } ``` `markApproved` is public, so the browser can skip `approve` and call it directly. Making it `protected` removes it from the callable list. ## Type-hinted parameters narrow the input, they do not authorize it Livewire invokes actions through its implicit binding helper, so a parameter typed as a model behaves like route model binding: `approve(Payslip $payslip)` turns the id the browser sent into a `Payslip` through `resolveRouteBinding()`, and a missing row throws `ModelNotFoundException`. Scalar types coerce or reject malformed values. Both are useful, and neither answers the real question: the bound payslip exists, but does it belong to a department this clerk may approve? Binding proves the record is real; only an authorization check proves the user may touch it. Treat the parameter list like a route's URI segments: typed, bound, and still attacker-chosen. ## Why Livewire does not guess Livewire cannot know which public methods you meant for the browser: templates are rendered at runtime, and Alpine code can call any action through `$wire`. So the rule is structural: **visibility is the allowlist**. The same thinking applies to public properties, which the client may update unless you lock them. ## Rules of thumb 1. Treat each public method as a route: it needs its own authorization and validation. 2. Keep helpers, queries and side-effect methods `protected` or `private`. 3. Audit traits: a trait that adds a public `delete()` to your component adds an endpoint. 4. Never rely on the absence of a button, a disabled attribute or `@can` in the view as the guard; those only shape the UI. 5. Type-hint parameters (`int $payslipId`, or a model class) so malformed values fail early, but remember a well-formed id can still belong to someone else. The payload guards in `config/livewire.php` (`payload.max_calls`, 50 by default) limit how many calls one request may carry; they limit abuse volume, not access.
- A teammate adds a trait with a public resetAll() method to a Livewire component. What changed?Livewire builds the callable list from public methods declared on your component class, and trait methods count as declared there. So `resetAll()` became an endpoint any user of that page can invoke with any parameters. Either make it `protected`, or give it the same authorization an action would need.
- What does the browser see when it calls a protected Livewire method in production?Livewire throws `MethodNotFoundException`. With `app.debug` false it renders an empty 419 response instead of an error page, so nothing about the component's internals leaks; in debug mode Laravel shows the full exception. The method itself never runs.
saying these in an interview costs you the question
- Only methods referenced by wire:click in the view can be called.
- Hiding the button with @can is enough to protect the action.
- Livewire validates action parameters against the values it rendered.
- Public methods from a trait are not exposed to the browser.
- Protected methods are callable as long as no template uses them.