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.
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.
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
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:
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:
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
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 — Post → PostPolicy.
If your namespacing follows this convention, you don't need to register anything manually.
For non-standard naming, register explicitly in AuthServiceProvider:
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.
// 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:
// 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:
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:
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.