Gate vs Policy — authorization at different granularities
Concept
Gate vs Policy — Laravel's two authorization mechanisms, operating at different granularities.
Gate: Simple closure-based authorization check. Good for actions not tied to a specific model (global permissions). Gate::define('view-dashboard', fn(User $user) => $user->isAdmin()).
Policy: A class dedicated to authorization logic for a SINGLE model. One policy = one model. Methods in the policy correspond to actions: view, create, update, delete, restore, forceDelete. Laravel auto-discovers policies using naming conventions (User model → UserPolicy).
When to use each:
- Gate: System-wide checks. "Can this user access the admin panel?" "Can this user export CSV?" Not model-specific.
- Policy: Resource-specific authorization. "Can this user edit THIS order?" "Can this user delete THIS comment?" The policy receives both the user AND the model instance.
Auto-discovery: If User model and UserPolicy class exist in the conventional locations, Laravel registers the policy automatically (no manual registration needed in Laravel 8+).
Guest users: By default, gates and policies return false for unauthenticated users. Define nullable user parameter (?User $user) to allow guest access.
before() method in Policy: Runs before any other check. Return true to grant access unconditionally (useful for super-admins). Return null to continue to the regular check.
Code Example
<?php
// GATE — simple, action-based (not model-specific)
// In AuthServiceProvider (or AppServiceProvider in L11)
use Illuminate\Support\Facades\Gate;
Gate::define('access-admin', fn(User $user): bool => $user->hasRole('admin'));
Gate::define('export-users', fn(User $user): bool => $user->hasPermission('users.export'));
// Using gates
if (Gate::allows('access-admin')) { /* ... */ }
if (Gate::denies('export-users')) abort(403);
// In a controller
public function export(): Response
{
Gate::authorize('export-users'); // throws 403 if denied
return Excel::download(new UsersExport, 'users.xlsx');
}
// In Blade
@can('access-admin')
<a href="/admin">Admin Panel</a>
@endcan
// POLICY — model-specific authorization
class OrderPolicy
{
// Called before all other methods — super-admin bypass
public function before(User $user, string $ability): ?bool
{
if ($user->isSuperAdmin()) return true; // grant everything
return null; // continue to specific method
}
// Can ANY authenticated user see ANY orders listing?
public function viewAny(User $user): bool { return true; }
// Can this user see THIS order?
public function view(User $user, Order $order): bool
{
return $user->id === $order->user_id || $user->hasRole('staff');
}
// Can this user UPDATE this order?
public function update(User $user, Order $order): bool
{
return $user->id === $order->user_id && $order->status === 'pending';
}
// Can this user create orders? (no model instance needed)
public function create(User $user): bool { return $user->hasVerifiedEmail(); }
}
// Register (or let Laravel auto-discover)
// In AuthServiceProvider:
protected $policies = [Order::class => OrderPolicy::class];
// Using policies in controller
class OrderController extends Controller
{
public function show(Order $order): JsonResponse
{
$this->authorize('view', $order); // uses OrderPolicy::view(auth()->user(), $order)
return response()->json($order);
}
public function update(Request $request, Order $order): JsonResponse
{
$this->authorize('update', $order); // throws 403 if denied
$order->update($request->validated());
return response()->json($order);
}
}
// In Blade
@can('update', $order)
<a href="/orders/{{ $order->id }}/edit">Edit</a>
@endcan
// Via Gate facade with model
Gate::authorize('view', $order);
Gate::allows('update', $order); // returns bool