Policies & Gates

Centralize authorization: who is allowed to do what, expressed as small, testable rules.

Authorization is not validation. Validation asks "is this data well-formed?"; authorization asks "is this user allowed to do this?" Laravel gives you two tools: Gates for simple, model-free checks and Policies for permissions grouped around a model.

Gates

A closure registered in a provider, keyed by an ability name. Good for coarse checks like view-admin-panel.

Gate::define('view-admin', fn ($user) => $user->is_admin);

if (Gate::allows('view-admin')) { ... }

Policies

A class whose methods (view, update, delete) map to abilities on a model. Laravel auto-discovers PostPolicy for Post. You then check with $user->can('update', $post), the @can Blade directive, or $this->authorize('update', $post) in a controller.

The before() hook & responses

A policy's before() can grant super-admins everything up front. And returning a Response::deny('message') instead of false gives the user a reason, not just a bare 403.

Example

Example · php
<?php

namespace App\Policies;

use App\Models\{Post, User};
use Illuminate\Auth\Access\Response;

class PostPolicy
{
    // Runs before every ability: super-admins bypass all checks.
    public function before(User $user, string $ability): ?bool
    {
        return $user->is_super_admin ? true : null; // null = defer to method
    }

    public function view(?User $user, Post $post): bool
    {
        // Guests (?User null) may view published posts.
        return $post->published || $user?->id === $post->user_id;
    }

    public function update(User $user, Post $post): Response
    {
        return $user->id === $post->user_id
            ? Response::allow()
            : Response::deny('You can only edit your own posts.');
    }

    public function delete(User $user, Post $post): Response
    {
        if ($post->published) {
            return Response::deny('Unpublish before deleting.');
        }
        return $user->id === $post->user_id ? Response::allow() : Response::denyAsNotFound();
    }
}

// --- Enforcing it ---
// In a controller:  $this->authorize('update', $post);
// In Blade:         @can('update', $post) ... @endcan
// On a route:       ->middleware('can:update,post');

When to use it

  • A PostPolicy's update() method checks that the authenticated user is the author of the post before allowing the update, centralising the ownership rule in one class.
  • An admin panel uses $this->authorize('viewAny', Post::class) in a controller to block non-admin users from seeing the post list without duplicating the check in every action.
  • A Blade template uses @can('delete', $post) to hide the delete button from users who do not have the required policy permission, keeping the view security-aware.

More examples

Generate and define a policy

The --model flag pre-fills all seven resource method stubs (viewAny, view, create, update, delete, restore, forceDelete) with the Post model type-hinted.

Example · bash
php artisan make:policy PostPolicy --model=Post
# Creates app/Policies/PostPolicy.php
# Register it in app/Providers/AuthServiceProvider.php

Policy method and registration

The policy methods receive the authenticated user and model instance; the AuthServiceProvider maps the model to the policy class.

Example · php
// app/Policies/PostPolicy.php
class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->isAdmin();
    }
}

// app/Providers/AuthServiceProvider.php
protected $policies = [
    Post::class => PostPolicy::class,
];

Authorise in a controller and Blade view

authorize() in the controller throws a 403 if the policy fails; @can in Blade conditionally renders UI elements based on the same policy.

Example · php
// In a controller
public function update(UpdatePostRequest $request, Post $post): RedirectResponse
{
    $this->authorize('update', $post); // 403 if policy returns false

    $post->update($request->validated());
    return redirect()->route('posts.show', $post);
}

{{-- In a Blade template --}}
@can('delete', $post)
    <form method="POST" action="{{ route('posts.destroy', $post) }}">
        @csrf @method('DELETE')
        <button>Delete</button>
    </form>
@endcan

Discussion

  • Be the first to comment on this lesson.