A policy is a class that holds every permission rule for one model, such as view, create, update and delete for a Post.
πIf gates are individual sticky notes on the fridge, a policy is the house rulebook with one chapter per room. Want to know what's allowed in the kitchen? Open the kitchen chapter.
Rules grow up with your app
Organised by model
PostPolicy, CommentPolicy⦠you always know where a rule lives.
Matches resource controllers
Policy methods line up with the seven resource actions, so CRUD and permissions fit together naturally.
Zero registration
Name the class correctly and Laravel finds it automatically.
How Laravel finds the right rule
| Controller action | Policy method | Needs a model instance? |
|---|---|---|
index | viewAny(User $user) | No |
show | view(User $user, Post $post) | Yes |
create / store | create(User $user) | No, pass Post::class |
edit / update | update(User $user, Post $post) | Yes |
destroy | delete(User $user, Post $post) | Yes |
Build a PostPolicy
-
Generate the policy
The
--modelflag fills in all the standard methods for you.php artisan make:policy PostPolicy --model=PostThis creates
app/Policies/PostPolicy.phpwithviewAny,view,create,update,delete,restoreandforceDelete. -
Write the rules
Each method gets the logged-in user (and usually the post) and returns a boolean. Delete the methods you don't need.
<?php namespace App\Policies; use App\Models\Post; use App\Models\User; class PostPolicy { public function create(User $user): bool { return $user->email_verified_at !== null; } 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; } }βΉοΈ Auto-discoveryLaravel matches
App\Models\PosttoApp\Policies\PostPolicyby name, so there's nothing to register. If you break the convention, register it by hand inAppServiceProvider::boot()withGate::policy(Post::class, PostPolicy::class). -
Authorize in the controller
Call
Gate::authorize()with the ability (the policy method name) and the model. For actions without a model, likecreate, pass the class name instead.use Illuminate\Support\Facades\Gate; public function store(Request $request) { Gate::authorize('create', Post::class); // validate + save... } public function update(Request $request, Post $post) { Gate::authorize('update', $post); // validate + update... }Prefer
$this->authorize('update', $post)? In Laravel 11+ the baseControlleris empty, so adduse AuthorizesRequests;(fromIlluminate\Foundation\Auth\Access) toapp/Http/Controllers/Controller.phpfirst. -
Or protect the route with the
canmiddlewareYou can authorize before the controller even runs. The second argument is the route parameter name, and route model binding turns it into a
Post.Route::put('/posts/{post}', [PostController::class, 'update']) ->middleware(['auth', 'can:update,post']); // fluent version Route::delete('/posts/{post}', [PostController::class, 'destroy']) ->middleware('auth') ->can('delete', 'post'); Route::post('/posts', [PostController::class, 'store']) ->can('create', App\Models\Post::class); -
Use the policy in Blade
@canworks exactly like it did with gates, but now it uses the policy.@can('update', $post) <a href="{{ route('posts.edit', $post) }}">Edit</a> @endcan @can('delete', $post) <form method="POST" action="{{ route('posts.destroy', $post) }}"> @csrf @method('DELETE') <button>Delete</button> </form> @endcan @can('create', App\Models\Post::class) <a href="{{ route('posts.create') }}">New post</a> @endcan -
Admins first: the policy
beforemethodA
before()method on the policy runs ahead of all its other methods. As withGate::before, returntrueto allow ornullto continue.public function before(User $user, string $ability): ?bool { return $user->is_admin ? true : null; }
Avoid this, do that
Gate::authorize('create', new Post);
Gate::authorize('create');
Without a model or class, Laravel doesn't know which policy to use, and an empty model is misleading.
Gate::authorize('create', Post::class);
Pass the class name for actions that don't have a specific record yet.
app/Policies/PostsPolicy.php
app/Policies/Post.php
Wrong names break auto-discovery, and every check silently returns false (403).
app/Policies/PostPolicy.php
Model name + Policy, in app/Policies. Let make:policy name it for you.
Return Response::deny('You can only edit your own posts.') (from Illuminate\Auth\Access\Response) instead of false to show a custom message, or Response::denyAsNotFound() to return a 404 so outsiders can't even tell the post exists.
- Run
php artisan make:policy PostPolicy --model=Postand fill inupdateanddelete. - Replace any
update-postgate from the previous lesson withGate::authorize('update', $post). - Protect the destroy route with
->can('delete', 'post'). - Show Edit/Delete buttons only with
@can, then test as two different users. - Create a
CommentPolicywhere both the comment author and the post author can delete a comment.
Key takeaways
- A policy groups all permission rules for one model.
make:policy PostPolicy --model=Postscaffolds the standard methods.App\Policies\PostPolicyis auto-discovered forApp\Models\Post.- Check with
Gate::authorize('update', $post),->can('update', 'post')on routes, or@canin Blade. - For βcreateβ-style checks, pass
Post::classinstead of a model.