livewire/livewire: Improve `#[Authorize]` argument resolution
Contributed by joshhanley
Part of the livewire v4.4.7 release
Pull request #10371 rewrites how Livewire 4's #[Authorize] attribute resolves action arguments. Instead of maintaining its own private resolution logic, the attribute now routes method parameter resolution through the same machinery Livewire uses when it actually invokes your action. For anyone upgrading from v3.8.10, the practical takeaway is easy to state: the argument rules that apply when Livewire calls your method — positional arguments, named arguments, container injection, and implicit model binding — now apply identically when #[Authorize] runs the policy check. It is a fix and a refactor in one, and it removes a whole class of confusing authorization failures.
What changed
Three things moved in this PR:
ImplicitlyBoundMethod, Livewire's internal helper for invoking actions, gained a publicresolveMethodDependencies()method. It resolves a method's dependencies once and returns them in two shapes: apositionallist (the flat argument list used to make the call) and anamedmap (parameter name to resolved value, including implicitly bound models).BaseAuthorize, the engine behind#[Authorize], now reads from that named map when one of the attribute's arguments matches a method parameter. The old code, which manually searched the incoming parameters by name or by signature position, calledresolveRouteBinding()itself, and threw its ownModelNotFoundExceptionon failure, is gone.- Resolution is lazy and memoized. The dependency set is computed on first use inside the attribute and reused for every argument in the check, so a multi-argument authorization like
#[Authorize('update', ['post', 'comment'])]triggers a single resolution pass.
Regular action calls are untouched: getMethodDependencies() now simply delegates to the new method and keeps returning the positional list it always did.
Why it matters
The old attribute path used the parameter's position in the method signature to find its value among the arguments sent from the browser. Those two positions are not the same thing once dependency injection enters the picture. Signature positions count injected dependencies; wire arguments do not.
Consider an action whose signature is editPost(UrlGenerator $generator, Post $post). The post parameter sits at signature position 1, but a call like wire:click="editPost({{ $post->id }})" sends a single argument that arrives at position 0. The old attribute looked for a parameter at index 1, found nothing, and failed to bind the model, even though calling the action itself worked perfectly. Two implementations of "how do we invoke this method" had drifted apart, and authorization was the one that lost.
Before and after
Here is the exact shape the PR's new tests cover:
use Illuminate\Routing\UrlGenerator;
use Livewire\Attributes\Authorize;
use Livewire\Component;
class EditPost extends Component
{
#[Authorize('edit', 'post')]
public function editPost(UrlGenerator $generator, Post $post): bool
{
// $generator is injected from the container,
// $post is implicitly bound from the action argument.
return $post->publish();
}
}
<button wire:click="editPost({{ $post->id }})">Publish</button>
Before this change, the #[Authorize('edit', 'post')] check could not resolve $post from the single id, because the injected UrlGenerator shifted the signature positions. After the change, the attribute resolves the full dependency set exactly as the invocation would: $generator comes from the container, $post is bound from the id, and the bound model is passed to the edit policy.
Both calling styles now work with dependencies in the signature:
Livewire::actingAs($user)->test(EditPost::class)
->call('editPost', 1) // positional id
->assertOk();
Livewire::actingAs($user)->test(EditPost::class)
->call('editPost', post: 1) // named argument
->assertOk();
The PR adds tests for all three variants: positional id, dependency injection plus positional id, and dependency injection plus named argument.
How it works under the hood
resolveMethodDependencies() accepts the container, a callable, and the raw parameters. It walks the method's parameters with a reflector and runs the same substitution steps a real invocation uses: name-binding substitution, implicit binding for model and enum parameters, then container injection for anything left over. As it walks, it records what each parameter resolved to in a named map, with variadic parameters collecting all of their dependencies.
BaseAuthorize then resolves each of the attribute's arguments with a simple priority. If the argument is a class name (think #[Authorize('create', Post::class)] for policies that do not need a model), that check still short-circuits first. If it matches a method parameter, the resolved value comes from the named map. Otherwise, the existing fallback to public component properties applies, unchanged from before.
Upgrade impact (v3.8.10 to v4)
- Nothing to migrate.
#[Authorize]is a Livewire 4 addition, so there is no v3 behavior to unlearn; you inherit the corrected semantics from day one. - Simple cases are unchanged.
editPost(Post $post)called witheditPost(1)resolved fine before and resolves identically now. - Early 4.x adopters who worked around the injection-plus-binding bug (reordering parameters so the model came first, or calling
$this->authorize()manually inside the action) can drop those workarounds. - If you caught the attribute-specific
ModelNotFoundExceptionto handle failed bindings, note that failures now surface through the shared resolution path used for action calls, so error behavior is consistent between authorizing and executing.
Takeaways
#[Authorize]and action invocation now share one argument-resolution code path, so they can no longer disagree.- Mixing container-injected dependencies with implicitly bound models works for both positional and named arguments.
- Dependency resolution is lazily memoized per authorization check, keeping multi-argument attributes to a single pass.
- Upgrading from v3.8.10 requires no code changes; expect the attribute to "see" exactly the arguments your action sees.