0

Gate vs Policy — authorization at different granularities

Beginner5 min read·eng-14-017
interviewlaravel-src

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
<?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