Laravel Policies and Gates Reference

A reference for Laravel policies and gates: authorizing what a user can do, beyond simple route middleware.

Gates vs policies

A gate is a simple closure-based authorization check, good for actions that aren't tied to a specific model (e.g. "can view the admin panel"). A policy groups all the authorization logic for one model — and is what most apps end up using once there's more than a rule or two.

Gates

// AppServiceProvider::boot()
Gate::define('view-admin-panel', function (User $user) {
    return $user->role === 'admin';
});
if (Gate::allows('view-admin-panel')) {
    // ...
}

Gate::authorize('view-admin-panel'); // throws 403 automatically if denied

Creating a policy

php artisan make:policy PostPolicy --model=Post
class PostPolicy
{
    public function view(User $user, Post $post): bool
    {
        return $post->published || $user->id === $post->user_id;
    }

    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->role === 'admin';
    }
}

Laravel auto-discovers a policy named {Model}Policy for a given model — no manual registration needed in modern Laravel versions, as long as it lives in the conventional app/Policies location.

Checking a policy

// In a controller
public function update(Request $request, Post $post)
{
    $this->authorize('update', $post); // 403s automatically if denied

    // ...
}

// Anywhere else
if ($user->can('update', $post)) {
    // ...
}

if ($user->cannot('delete', $post)) {
    abort(403);
}

In Blade

@can('update', $post)
    <a href="/posts/{{ $post->id }}/edit">Edit</a>
@endcan

@cannot('delete', $post)
    <p>You cannot delete this post.</p>
@endcannot

Authorizing in Form Requests

class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', $this->route('post'));
    }
}

Because a Form Request's authorize() method runs before validation, a denied request fails fast with a 403 and never touches the validation rules. This is a good place to check ownership or role-based access without cluttering the controller method itself.

Resource controllers and authorizeResource

A policy's method names line up with the seven resource controller actions — viewAny, view, create, update, delete, plus restore and forceDelete for soft-deleted models. Rather than calling $this->authorize() in every method, a resource controller can authorize all of them in one line via its constructor:

class PostController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Post::class, 'post');
    }

    // viewAny() is checked before index(), view() before show(),
    // update() before update(), delete() before destroy(), and so on.
}

Note that viewAny and create don't receive a model instance — there's nothing to check ownership against yet — so those two methods only take the $user argument.

Gate::before and Gate::after

Both gates and policies can be short-circuited globally. A common use is granting a super-admin role blanket access without having to repeat that check in every single policy method:

// AppServiceProvider::boot()
Gate::before(function (User $user, string $ability) {
    if ($user->isAdmin()) {
        return true; // skips the specific gate/policy check entirely
    }
});

Gate::after(function (User $user, string $ability, ?bool $result, mixed $arguments) {
    // Runs after every check, even ones already resolved by before().
    // Useful for auditing/logging authorization decisions.
});

Returning null (or nothing) from a before callback lets the normal gate or policy method decide as usual; returning true or false overrides it immediately.

Custom denial messages

By default a failed authorization check just throws a generic 403. To surface a more specific reason to the user, return a Response object from the policy method instead of a boolean:

use Illuminate\Auth\Access\Response;

public function update(User $user, Post $post): Response
{
    return $user->id === $post->user_id
        ? Response::allow()
        : Response::deny('You do not own this post.');
}

The custom message is available on the caught AuthorizationException and is what gets shown when $this->authorize() aborts the request.