Define gates in AppServiceProvider::boot(); each closure receives the user, then your arguments. Gate::before runs ahead of every gate and policy, and Gate::after once the answer is known:
Gate::before(fn (User $user) => $user->hasRole('admin') ? true : null);
Gate::define('view-dashboard', fn (User $user) => $user->hasRole('staff'));
Gate::define('refund-order', fn (User $user, Order $order) => match (true) {
! $user->hasRole('staff') => Response::deny('Only staff can issue refunds.'),
$order->status !== 'paid' => Response::deny("Order {$order->id} is {$order->status}."),
default => Response::allow(),
});
Gate::after(fn (User $user, string $ability, mixed $result) => Log::info(
"gate {$ability} user={$user->name} result=".json_encode(
$result instanceof Response ? $result->allowed() : $result)));Gate::allows() and denies() check the current user, Gate::forUser($user) someone else, and any() or none() a list of abilities. In php artisan tinker:
[$paid, $refunded] = [App\Models\Order::find(1), App\Models\Order::find(2)];
$refunded->update(['status' => 'refunded']);
foreach (App\Models\User::whereIn('name', ['Ann', 'Sam', 'Ada'])->get() as $user) {
$gate = Gate::forUser($user);
printf("%s dashboard=%d refund(1)=%d refund(2)=%d\n", $user->name,
$gate->allows('view-dashboard'), $gate->allows('refund-order', $paid),
$gate->allows('refund-order', $refunded));
}Ann dashboard=0 refund(1)=0 refund(2)=0 Sam dashboard=1 refund(1)=1 refund(2)=0 Ada dashboard=1 refund(1)=1 refund(2)=1
Ada may refund an order twice: before answered true, so the rule never ran; keep before for true superusers. The after hook logged gate refund-order user=Sam result=false and the like. Type its $result as mixed: a rule's Response object arrives there, and the documented ?bool hint throws a TypeError.
A role column suffices while each user has one role. To grant permissions without a deploy, use spatie/laravel-permission 12,967 (https://github.com/spatie/laravel-permission 12,967 ) (MIT; 8.3.0 of 3 July 2026, for Laravel 12 2,157 and 13). It registers its own Gate::before: once Sam held a role with the moderate reviews permission, $sam->can('moderate reviews') returned true, no gate needed.
| Concern | role column | spatie/laravel-permission |
|---|---|---|
| Storage | One string per user | Five tables, many roles |
| Changing a role's rights | Edit code, deploy | Update rows |
| Route middleware | can: only | Adds role:, permission: |
| Extras | None | Cache, teams, guards, wildcards |