skip to content

What does Livewire's #[Authorize] attribute do on a component action, and how does it find the model to check?

level: middleimportance: should knowfreq 30%

answer

  1. Gate check before the method body
  2. ability plus optional argument
  3. class string, method parameter, then property
  4. type-hint required for parameter models
  5. repeatable; failure means 403

basics

~10 s

#[Authorize('ability', 'argument')] runs a Gate check before a Livewire action executes and returns 403 on failure. The argument resolves as a class name, a type-hinted method parameter, or a component property.

solid answer

~40 s

`#[Authorize]` wires Laravel's Gate into a Livewire action: before the method body runs, Livewire calls the component's `authorize()` with the ability and a resolved argument, and a denial throws `AuthorizationException`, a 403. With no argument it checks a plain gate; a class name such as `Payslip::class` is passed as-is for `create`-style checks; otherwise Livewire looks for a method parameter of that name, which must be type-hinted so it can bind the model, and falls back to a component property via `data_get`. An array argument's first element picks the policy and the rest become extra policy parameters. The attribute is repeatable, so every stacked check must pass. It guards server execution only; the view still needs `@can` to hide the button.

go deeper

for a junior

Recall the shape: #[Authorize('ability', 'argument')] above a public action, and that a denied check returns 403 before the method runs.

for a middle

Walk through argument resolution: no argument, class string, typed method parameter, then component property, and what an array argument does.

for a senior

Choose between the attribute and an explicit authorize() call per action, and pair it with #[Locked] or model properties, since it guards calls, not updates.

for a principal

Decide whether declarative per-action checks should be a team rule for Livewire code, and how reviews verify every public action carries one.

## What the attribute is Every public method on a **Livewire component** is callable from the browser, so each action needs its own authorization. The classic way is to call `$this->authorize('approve', $payslip)` at the top of the method; `Livewire\Component` uses Laravel's `AuthorizesRequests` trait, so the helper is there. Livewire 4 also ships **`Livewire\Attributes\Authorize`**, a PHP attribute that performs the same Gate check declaratively, before the method body runs. ```php #[Authorize('approve', 'payslip')] public function approve(Payslip $payslip) { ... } ``` Under the hood the attribute hooks Livewire's call pipeline: when the browser names the method, the attribute's `call()` runs first and invokes `authorize()` with the ability and the resolved argument. If the Gate denies, Laravel throws `AuthorizationException`, which the exception handler turns into a **403 Forbidden**, and the method never runs. ## How the argument is resolved The attribute takes `ability` (a string or a backed enum) and an optional `argument`. Livewire resolves it in this order: | Argument | Resolution | Typical use | |---|---|---| | none | Plain gate, no model | `#[Authorize('view-payroll')]` | | an existing class name | Passed through as the class | `#[Authorize('create', Payslip::class)]` | | a name matching a method parameter | The parameter's bound value, resolved like a route binding | `approve(Payslip $payslip)` | | any other string | `data_get($component, $name)`, a component property | `public Payslip $payslip` | For the parameter case, the **type-hint is required**: Livewire uses it to turn the id the browser sent into a model. An untyped `$payslip` leaves the policy looking at a raw id, and the check fails. Because the parameter path runs before the action, a missing row fails at binding time too. An **array** argument follows Laravel's policy convention: `#[Authorize('create', [Comment::class, 'payslip'])]` sends the check to the `Comment` policy and passes the `payslip` property as an extra argument. ## Stacking and scope The attribute is declared `IS_REPEATABLE`, so several can sit on one method: ```php #[Authorize('view-payroll')] #[Authorize('approve', 'payslip')] public function approve(Payslip $payslip) { ... } ``` All of them must pass. It targets methods only, so it cannot protect a property update; that is what `#[Locked]` or a model-typed property is for. ## Timing: after updates, before the body Within one Livewire request, property **updates** are applied first and **calls** run afterwards. So when the attribute resolves a component property, it sees the value after this request's updates, which is the same value the method body will use. That is the right behaviour for authorization, because the check covers exactly what the action touches. It also means the attribute never protects a property's integrity by itself: - With `public Payslip $payslip`, the value is rebuilt from the signed snapshot, so the check and the body both see the payslip the page was mounted with. - With a plain `public $payslipId`, a tampered id is checked against the policy, which then denies it if the user may not approve that payslip. The check still holds, but only because the policy is right. The explicit call gives the same guarantee when it is the method's first line; the attribute simply makes that placement impossible to get wrong. ## Attribute or explicit call? - **Attribute**: short, visible in a code review, impossible to forget to call after a refactor moves the body around. - **Explicit `authorize()`**: needed when the object to check is only known midway, for example after a query that depends on other input, or when the decision depends on the update being applied first. - **Neither hides UI**: the Blade view still needs `@can('approve', $payslip)` so users do not see buttons that will 403. ## Testing it In a component test, `Livewire::actingAs($user)->test(...)` followed by `->call('approve', $payslip->id)->assertForbidden()` proves the check fires. Writing the policy itself, and which user may approve which payslip, is Laravel's authorization layer, not Livewire's. ## Common mistakes 1. Using the property name when the method parameter has the same name: the **parameter wins**, which surprises people who meant the property. 2. Forgetting the type-hint on a parameter-resolved model. 3. Assuming the attribute also stops property tampering between calls. 4. Assuming a stacked list is "any of": it is "all of".

  • A Livewire action has both a parameter and a public property named $payslip, with #[Authorize('approve', 'payslip')]. Which is checked?
    The method parameter. Livewire's resolver looks for a method parameter with that name first and falls back to a component property only when none matches. So the check runs against whatever the browser passed, bound through the type-hint, not the property the component was mounted with.
  • Can #[Authorize] protect a Livewire property from being changed by the browser?
    No. It targets methods and runs only when that method is called. A property update is applied before any call, and nothing on the property itself is authorized. Use `#[Locked]` or a model-typed property for that, and keep the action's authorization for what the user may do.

saying these in an interview costs you the question

  • #[Authorize] hides the button from users who lack the ability.
  • Stacked #[Authorize] attributes pass if any one of them passes.
  • An untyped parameter is bound to a model automatically.
  • A failed #[Authorize] check silently skips the method and returns 200.
  • #[Authorize] also blocks client updates to the named property.