In one sentence

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.

Why it matters

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.

The big picture

How Laravel finds the right rule

❓can('update', $post)ability + model
πŸ”Ž$post is a PostApp\Models\Post
πŸ“˜PostPolicyApp\Policies\PostPolicy
βš–οΈupdate($user, $post)returns true / false
Ability name β†’ policy method. The model's class decides which policy is used.
Controller actionPolicy methodNeeds a model instance?
indexviewAny(User $user)No
showview(User $user, Post $post)Yes
create / storecreate(User $user)No, pass Post::class
edit / updateupdate(User $user, Post $post)Yes
destroydelete(User $user, Post $post)Yes
Step by step

Build a PostPolicy

  1. Generate the policy

    The --model flag fills in all the standard methods for you.

    
              php artisan make:policy PostPolicy --model=Post
            

    This creates app/Policies/PostPolicy.php with viewAny, view, create, update, delete, restore and forceDelete.

  2. 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-discovery

    Laravel matches App\Models\Post to App\Policies\PostPolicy by name, so there's nothing to register. If you break the convention, register it by hand in AppServiceProvider::boot() with Gate::policy(Post::class, PostPolicy::class).

  3. Authorize in the controller

    Call Gate::authorize() with the ability (the policy method name) and the model. For actions without a model, like create, 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 base Controller is empty, so add use AuthorizesRequests; (from Illuminate\Foundation\Auth\Access) to app/Http/Controllers/Controller.php first.

  4. Or protect the route with the can middleware

    You 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);
            
  5. Use the policy in Blade

    @can works 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
            
  6. Admins first: the policy before method

    A before() method on the policy runs ahead of all its other methods. As with Gate::before, return true to allow or null to continue.

    
              public function before(User $user, string $ability): ?bool
              {
                  return $user->is_admin ? true : null;
              }
            
Common mistakes

Avoid this, do that

❌ Avoid

          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.

βœ… Do

          Gate::authorize('create', Post::class);
        

Pass the class name for actions that don't have a specific record yet.

❌ Avoid

          app/Policies/PostsPolicy.php
          app/Policies/Post.php
        

Wrong names break auto-discovery, and every check silently returns false (403).

βœ… Do

          app/Policies/PostPolicy.php
        

Model name + Policy, in app/Policies. Let make:policy name it for you.

πŸ’‘ Pro tip: friendlier denials

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.

πŸ‹οΈ Try it yourself
  1. Run php artisan make:policy PostPolicy --model=Post and fill in update and delete.
  2. Replace any update-post gate from the previous lesson with Gate::authorize('update', $post).
  3. Protect the destroy route with ->can('delete', 'post').
  4. Show Edit/Delete buttons only with @can, then test as two different users.
  5. Create a CommentPolicy where both the comment author and the post author can delete a comment.
Recap

Key takeaways

Check yourself

Mini quiz