01 — IntroThe Authorization Problem

Almost every application needs to answer one fundamental question before serving a request: is this user allowed to do this? Laravel provides two elegant tools to answer it — Gates and Policies — and a surprising number of developers use them interchangeably, or reach for one by habit without thinking about why.

That's a mistake worth correcting. The two primitives exist for different reasons, live in different layers of your application, and serve different scales of complexity. Using the wrong one isn't catastrophic, but it creates friction — logic scattered in the wrong places, tests that are harder to write, and onboarding nightmares for the next developer on your team.

This article walks through both from first principles, shows you real-world code, and gives you a decision framework you can apply immediately.


02 — GatesWhat Are Gates?

Gates are closures. That's the mental model. They are simple, named callbacks that receive a user (and optionally some additional arguments) and return true or false.

You define Gates in a service provider — typically AuthServiceProvider — using the Gate facade. They're global, stateless functions that answer boolean questions about authorization without being tied to any particular Eloquent model or resource.

Think of Gates as the authorization equivalent of a route closure: great for one-off logic, quick to write, and best for simple or non-model-specific checks.


03 — PoliciesWhat Are Policies?

Policies are classes. Each Policy is a PHP class that groups all the authorization logic related to a specific Eloquent model. A PostPolicy holds every permission question about Post models: can this user view it? Edit it? Delete it? Restore it?

Laravel auto-discovers policies that follow its naming convention, and you can inject dependencies into them via the constructor just like any other class. They support before hooks, guest user handling, and integrate naturally with resource controllers.

Policies are just organized Gates — but that organization pays dividends at scale.

If Gates are index cards, Policies are filing cabinets. Neither is better in the abstract; it depends entirely on how much you have to file.


04 — ComparisonGates vs. Policies: The Breakdown

Attribute Gates Gate Policies Policy
Structure Closure / callable PHP class
Defined in AuthServiceProvider Dedicated *Policy.php file
Tied to a model? No — general purpose Yes — one policy per model
Dependency injection Limited (closure scope only) Full constructor DI
Auto-discovery No Yes (naming convention)
Resource controller integration Manual First-class via authorizeResource()
Testability Moderate High — instantiate & call directly
Best for Simple, global, non-model checks Complex, model-specific CRUD rules

05 — CodeGates in Action

Define a Gate inside your AuthServiceProvider's boot() method. The first argument is the Gate's name; the second is a closure that receives the authenticated user as its first parameter.

app/Providers/AuthServiceProvider.php PHP
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    // Simple admin check — no model involved
    Gate::define('access-admin', function (User $user): bool {
        return $user->is_admin;
    });

    // Gate with an extra argument
    Gate::define('publish-to-section', function (User $user, Section $section): bool {
        return $user->hasPermission('publish', $section);
    });

    // Gate defined via invokable class (cleaner for complex logic)
    Gate::define('manage-billing', ManageBillingGate::class);
}

Using Gates in Controllers

app/Http/Controllers/AdminController.php PHP
public function dashboard(): Response
{
    // Throws 403 if check fails
    Gate::authorize('access-admin');

    return view('admin.dashboard');
}

public function publish(Section $section): Response
{
    // Returns bool — handle manually
    if (Gate::denies('publish-to-section', $section)) {
        abort(403, 'You cannot publish to this section.');
    }
    // ...
}

💡 Pro Tip

You can also check Gates on the authenticated user directly: $user->can('access-admin'). This works because Laravel's User model uses the Authorizable trait, which delegates to the Gate under the hood.


06 — CodePolicies in Action

Generate a Policy with Artisan. The --model flag scaffolds all standard CRUD methods for you:

Terminal Bash
php artisan make:policy PostPolicy --model=Post

Laravel creates app/Policies/PostPolicy.php with methods for viewAny, view, create, update, delete, restore, and forceDelete. Fill in your logic:

app/Policies/PostPolicy.php PHP
class PostPolicy
{
    /**
     * Before hook — runs before all other checks.
     * Returning true grants the action immediately.
     */
    public function before(User $user, string $ability): bool|null
    {
        if ($user->is_super_admin) {
            return true; // super admins bypass everything
        }
        return null; // defer to the specific method
    }

    /** Any logged-in user can list posts */
    public function viewAny(User $user): bool
    {
        return true;
    }

    /** Any user can view a published post; authors can view drafts */
    public function view(User $user, Post $post): bool
    {
        return $post->published || $user->is($post->author);
    }

    /** Only editors and above can create */
    public function create(User $user): bool
    {
        return $user->hasRole('editor');
    }

    /** Only the post's author or an admin can update */
    public function update(User $user, Post $post): bool
    {
        return $user->is($post->author) || $user->hasRole('admin');
    }

    /** Only admins can hard-delete */
    public function delete(User $user, Post $post): bool
    {
        return $user->hasRole('admin');
    }
}

Using Policies in Controllers

app/Http/Controllers/PostController.php PHP
class PostController extends Controller
{
    public function __construct()
    {
        // Automatically authorize all resource methods via the Policy
        $this->authorizeResource(Post::class, 'post');
    }

    // All CRUD methods are now covered. Zero repeated Gate::authorize() calls.

    public function update(Request $request, Post $post): Response
    {
        // No manual auth check needed — authorizeResource handled it
        $post->update($request->validated());
        return back();
    }
}

07 — SetupRegistering & Auto-Discovery

Since Laravel 8, policy auto-discovery is enabled by default. Laravel looks for a Policy in App\Policies that matches the model name — PostPostPolicy. If your namespacing follows this convention, you don't need to register anything manually.

For non-standard naming, register explicitly in AuthServiceProvider:

app/Providers/AuthServiceProvider.php PHP
protected $policies = [
    Post::class  => PostPolicy::class,
    Order::class => OrderManagementPolicy::class, // non-standard name
];

⚠️ Watch Out

If you use custom policy discovery logic (e.g., organizing policies into subdirectories like Policies/Blog/PostPolicy), you'll need to register a custom discovery callback via Gate::guessPolicyNamesUsing(). Forgetting this is a common source of silent 403 errors.


08 — UsageRunning Authorization Checks

Gates and Policies share the same authorization API — which is deliberate. You use the same methods regardless of which underlying mechanism handles the check.

Authorization API — works for both Gates and Policies PHP
// In controllers (via $this->authorize helper from AuthorizesRequests trait)
$this->authorize('update', $post);         // throws 403 if denied
$this->authorize('create', Post::class);   // model-less check (create, viewAny)

// On the Gate facade directly
Gate::allows('update', $post);             // bool
Gate::denies('delete', $post);             // bool
Gate::authorize('update', $post);          // throws AuthorizationException

// On the User model
$user->can('update', $post);               // bool
$user->cannot('delete', $post);            // bool

// In Blade templates
// @can('update', $post)  ...  @endcan
// @cannot('delete', $post)  ...  @endcannot
// @canany(['update', 'delete'], $post)  ...  @endcanany

09 — AdvancedPatterns Worth Knowing

Guest Users in Policies

By default, all Policy methods are skipped for unauthenticated users (and return false). To allow guests through for specific checks, type-hint the user as nullable:

app/Policies/PostPolicy.php PHP
// The ? makes this check run even for guests
public function view(?User $user, Post $post): bool
{
    return $post->published;
}

Policy Responses with Custom Messages

Instead of returning a plain boolean, return a Response object for richer feedback:

app/Policies/PostPolicy.php PHP
use Illuminate\Auth\Access\Response;

public function delete(User $user, Post $post): Response
{
    return $user->is($post->author)
        ? Response::allow()
        : Response::deny('You can only delete your own posts.');
}

Testing Policies

One of Policies' biggest advantages is testability. Instantiate the Policy directly and call its methods — no HTTP overhead, no mocking required:

tests/Unit/PostPolicyTest.php PHP
it('allows authors to update their own posts', function () {
    $author   = User::factory()->create();
    $post     = Post::factory()->for($author, 'author')->create();
    $policy   = new PostPolicy();

    expect($policy->update($author, $post))->toBeTrue();
});

it('prevents other users from updating the post', function () {
    $other  = User::factory()->create();
    $post   = Post::factory()->create();
    $policy = new PostPolicy();

    expect($policy->update($other, $post))->toBeFalse();
});

10 — DecisionThe Framework: Which Do You Reach For?

Here's the mental flowchart, distilled to a single test: does the permission question revolve around a specific Eloquent model? If yes, write a Policy. If no, write a Gate. Everything else follows from there.

Reach for a Gate when…

  • The check isn't tied to a model ("is this user an admin?")
  • It's a one-off, application-level permission
  • You need a quick answer without a full class
  • The logic is truly global (billing, feature flags)
  • You're prototyping or in early development

Reach for a Policy when…

  • The permission relates to a model ("can she edit this post?")
  • You have multiple actions on the same resource
  • You're using a resource controller
  • You want clean, isolated unit tests
  • The logic is complex enough to need DI

There is one grey area worth naming: sometimes you have model-related checks that don't fit neatly into CRUD. A publishPost action, for example, could live in a Policy as a custom method — and that's perfectly fine. Policies don't have to be limited to the seven default methods. Add any named method you need and call it with $this->authorize('publish', $post).

📌 Rule of Thumb

Start with Gates for speed. When a Gate file gets long, or when you catch yourself writing multiple Gates that all reference the same model, that's the signal to extract a Policy. The refactor is mechanical and painless — and you'll know it's the right time.