Laravel Magazine
Gates vs. Policies vs. Middleware: Understanding Laravel's Authorization Layers

Gates vs. Policies vs. Middleware: Understanding Laravel's Authorization Layers

Eric Van Johnson ·

Ask a room of Laravel developers "where does authorization logic go?" and you'll get three different answers, all correct: Gates, Policies, and middleware. That's not redundancy in the framework — each one answers a structurally different question, and knowing which is which keeps a growing app's authorization logic from turning into an unmaintainable mix of if statements scattered across controllers.

The actual question each layer answers

Middleware answers: "can this request reach this route at all?" It runs before a controller method is even resolved, and it typically deals in coarse, request-level facts — is there an authenticated user, does that user have a given role, is the request coming from a verified account.

Gates answer: "can the current user perform this action, in general?" Gates are for authorization checks that aren't tied to a specific Eloquent model — "can this user access the admin panel," "can this user impersonate other users," "can this user export data." They're closures registered in a service provider, and they're the right tool exactly when there's no model instance to check against.

Policies answer: "can the current user perform this action, on this specific record?" Every Policy method takes the model instance as an argument, because the answer depends on it — "can this user edit this invoice" depends on whether this user owns this invoice, not on some general permission.

Why conflating them causes problems

The trouble starts when a Gate closure grows a model parameter and starts doing Policy work, or when middleware starts reaching into the database to check row-level ownership. Both are symptoms of skipping the layer that actually fits the question.

// This is a Gate check wearing a Policy's job — it needs the specific invoice
Gate::define('edit-invoice', function ($user, $invoiceId) {
    $invoice = Invoice::find($invoiceId);
    return $user->id === $invoice->client->account_manager_id;
});

That's not wrong, exactly — it works — but it's fighting the framework's grain. The moment you're loading a model inside a Gate closure to check per-record ownership, you've written a Policy method with extra steps and none of the automatic model resolution Policies give you for free.

class InvoicePolicy
{
    public function update(User $user, Invoice $invoice): bool
    {
        return $user->id === $invoice->client->account_manager_id;
    }
}

Same logic, but now $this->authorize('update', $invoice) in a controller resolves the right Policy automatically based on the Invoice type hint, @can('update', $invoice) works in Blade without extra wiring, and form request classes can call $this->user()->can('update', $this->invoice) the same way. You get all of that by putting the logic in the layer designed for "action plus specific model," instead of bolting model resolution onto a Gate.

Where middleware still earns its place

None of this means Policies replace middleware. Middleware is the right layer specifically because it runs before routing finishes resolving controller dependencies, which matters for checks that should block a request before any model binding, validation, or controller logic executes at all:

Route::middleware(['auth', 'verified', 'role:account-manager'])
    ->group(function () {
        Route::get('/invoices', InvoiceIndexController::class);
    });

Trying to do this check inside a controller method means Laravel has already done route model binding, resolved dependencies, and started executing controller logic before you find out the request should never have been allowed through. Middleware stops that earlier, and for broad, request-shaped questions like "is this user logged in" or "does this user hold this role," that's exactly the right place for the check to live.

A rule of thumb

If the check needs a specific model instance to evaluate, it's a Policy. If it's about the user's general standing or permissions with no specific record involved, it's a Gate. If it's about whether the request should be allowed to proceed at all before any of that logic even runs, it's middleware. Three tools, three different scopes, and the authorization code stays legible precisely because each piece of logic lives at the layer that was built to answer its specific question.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.