Laravel

Mastering Laravel Policies for Clean Authorization

ME
Mohamed Elogail
5 hours ago
10 min read
Mastering Laravel Policies for Clean Authorization

Mastering Laravel Policies for Clean Authorization

What You Will Learn

  • The difference between authentication and authorization in Laravel applications
  • Why authorization logic becomes difficult to manage in growing codebases
  • How Laravel Policies organize model-based authorization rules
  • When to use Policies instead of Gates
  • How to create, register, and use policies in controllers, routes, and Blade templates
  • How to write custom authorization methods for real application needs
  • How Laravel handles failed authorization checks
  • How to test policies to keep your application secure and maintainable

Authorization is one of the most important parts of any web application. As soon as your app has users, roles, or private resources, you need a reliable way to decide who can do what. In Laravel, Policies provide a clean and structured solution for this problem.

This article explains Laravel Policies in a practical way. You will learn what they are, why they matter, how they compare to Gates, and how to use them in controllers, Blade templates, and routes. Along the way, we will build a realistic post management example and cover testing strategies as well.

Introduction to Authorization in Web Applications

Modern applications usually deal with multiple users interacting with shared data. A customer should only see their own orders, an author should edit their own posts, and an administrator may need permission to manage everything. These decisions are part of authorization.

It is important to separate authorization from authentication. Authentication answers the question, Who are you? Authorization answers the question, What are you allowed to do? A user may be logged in successfully, but that does not mean they can update another user’s content or access administrative pages.

Without strong authorization rules, applications become insecure very quickly. Sensitive data may leak, resources may be modified by the wrong users, and business rules may be bypassed. That is why authorization should be treated as a core part of the application design, not an afterthought.

The Authorization Problem in Growing Applications

In small projects, developers often place permission checks directly inside controllers or service classes. At first this may seem simple. For example, a controller may check whether the authenticated user owns a post before updating it.

if ($post->user_id !== auth()->id()) {
    abort(403);
}

This works for one action, but problems appear as the project grows:

  • The same logic gets repeated in multiple controllers
  • Authorization rules become inconsistent across the application
  • Controllers become too large and hard to read
  • Changing a rule requires updates in many places
  • Testing authorization becomes more difficult

When authorization logic is scattered everywhere, maintenance becomes expensive. Developers can also accidentally forget a permission check on a new endpoint. Laravel Policies solve this by giving authorization a dedicated home.

What Are Laravel Policies

Laravel Policies are classes that organize authorization logic around a specific model or resource. Instead of writing permission checks everywhere, you define them in one policy class.

For example, a PostPolicy contains methods that answer questions like:

  • Can this user view this post?
  • Can this user update this post?
  • Can this user delete this post?

Policies usually map directly to Eloquent models. A Post model gets a PostPolicy, an Order model gets an OrderPolicy, and so on. This structure keeps authorization rules grouped by resource, which makes them easier to understand and maintain.

Why Laravel Policies Are Important

Policies are important because they improve both code quality and application security. They create a clear separation between application behavior and permission rules.

  • Cleaner controllers: Controllers focus on handling requests and responses, not permission logic
  • Centralized authorization: Rules for a resource live in one place
  • Better maintenance: Updating a rule does not require searching through many files
  • Easier testing: Authorization can be tested independently
  • Consistent behavior: The same rule is reused across controllers, routes, and views

In larger Laravel applications, these benefits become even more valuable. Policies help teams maintain order as business rules become more complex.

Policies vs Gates in Laravel

Laravel provides both Gates and Policies for authorization. They are related, but they serve different use cases.

Gates are best for simple authorization checks that are not strongly tied to a specific model. For example, deciding whether a user can access an admin dashboard is a good candidate for a Gate.

use Illuminate\Support\Facades\Gate;

Gate::define('access-admin', function ($user) {
    return $user->role === 'admin';
});

Policies are better for model-based authorization. If your rules depend on both the current user and a specific resource, such as a post or order, a policy is usually the right choice.

Use Gates when the rule is general and simple. Use Policies when the rule belongs to a resource and should be grouped with similar abilities.

When You Should Use Laravel Policies

Policies are most useful when your application has resources that users interact with directly. Common examples include:

  • Blog posts that authors can edit
  • Orders that customers can view
  • Support tickets assigned to agents
  • Comments that owners or moderators can remove
  • Projects that team members can update
  • Files that only authorized users can download

If a resource has actions like view, create, update, delete, approve, publish, or archive, a policy is often the most natural place for those checks.

Creating a Policy Class

Laravel makes policy creation simple with Artisan. You can generate a policy using this command:

php artisan make:policy PostPolicy --model=Post

This command creates a policy class in the app/Policies directory. Because we used the --model option, Laravel also generates the common policy methods for the Post model.

A typical generated file looks like this:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function viewAny(User $user): bool
    {
        return false;
    }

    public function view(User $user, Post $post): bool
    {
        return false;
    }

    public function create(User $user): bool
    {
        return false;
    }

    public function update(User $user, Post $post): bool
    {
        return false;
    }

    public function delete(User $user, Post $post): bool
    {
        return false;
    }
}

Registering Policies in Laravel

For Laravel to know which policy belongs to which model, you register the mapping in the AuthServiceProvider.

<?php

namespace App\Providers;

use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot(): void
    {
        $this->registerPolicies();
    }
}

Laravel also supports automatic policy discovery in many cases. If your policy follows naming conventions and is placed in the expected directory, Laravel can often find it automatically. Still, it is useful to understand explicit registration because it makes the mapping very clear.

Understanding the Structure of a Policy Class

A policy method usually receives two things:

  • The authenticated User
  • The target model, when the action is tied to a specific resource

For example, the update method in a post policy receives both a User and a Post. This gives the policy enough information to decide whether the action is allowed.

public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

Some actions do not require a target model. For example, create only needs the current user because no resource exists yet.

public function create(User $user): bool
{
    return $user->is_active;
}

Default Policy Methods Explained

When Laravel generates a policy with a model, it often includes several standard methods. Each method represents a common action.

  • viewAny: Can the user list resources? Example: view all posts
  • view: Can the user see a specific resource?
  • create: Can the user create a new resource?
  • update: Can the user edit an existing resource?
  • delete: Can the user remove a resource?
  • restore: Can the user restore a soft-deleted resource?
  • forceDelete: Can the user permanently delete a resource?

These methods align well with CRUD-based applications. They also integrate naturally with controllers, route middleware, and Blade directives.

Writing Authorization Logic Inside Policies

The policy method body contains the authorization rule itself. In many applications, the simplest rule is ownership. A user can update a resource only if they created it.

public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id;
}

You can also combine ownership checks with roles or permissions.

public function delete(User $user, Post $post): bool
{
    return $user->role === 'admin' || $user->id === $post->user_id;
}

Or create stricter rules based on application state.

public function update(User $user, Post $post): bool
{
    return $user->id === $post->user_id && $post->status !== 'published';
}

The key idea is to keep the logic focused on authorization only. If you start adding unrelated business operations, the policy becomes harder to maintain.

Adding Custom Policy Methods

You are not limited to Laravel’s default method names. Policies can define any custom ability your application needs.

For example, a publishing workflow might require abilities like:

  • publish
  • approve
  • archive
  • download
public function publish(User $user, Post $post): bool
{
    return in_array($user->role, ['editor', 'admin']);
}

public function archive(User $user, Post $post): bool
{
    return $user->role === 'admin';
}

These custom methods can be called with the same authorization tools Laravel provides for standard policy methods.

Using Policies in Controllers

One of the most common ways to use a policy is with the authorize helper inside a controller method. This checks the policy before the action continues.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        $post->update($request->only('title', 'body'));

        return redirect()->route('posts.show', $post);
    }
}

This keeps the controller short and readable. Instead of embedding role checks in the controller, the method simply asks Laravel to verify the policy.

You can also authorize class-level actions like create:

$this->authorize('create', Post::class);

Using Policies in Blade Templates

Policies are also useful in the view layer. Blade provides directives such as @can and @cannot so you can show or hide UI elements based on authorization rules.

@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 type="submit">Delete</button>
    </form>
@endcan

This helps the interface match the user’s permissions. However, hiding a button is not enough for security. You must still enforce the policy on the server side in controllers or routes.

Protecting Routes Using Policies

Laravel lets you enforce authorization at the route level using the can middleware. This is a clean way to block unauthorized access before the controller method runs.

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::put('/posts/{post}', [PostController::class, 'update'])
    ->middleware('can:update,post');

Route::delete('/posts/{post}', [PostController::class, 'destroy'])
    ->middleware('can:delete,post');

This approach works especially well when you want route definitions to clearly express access rules.

Handling Authorization Failures

When a policy check fails, Laravel throws an authorization exception. By default, the framework converts this into a 403 Forbidden HTTP response.

This is useful because you do not need to manually write abort(403) everywhere. Laravel handles the failure consistently.

From the user’s perspective, a denied action means they are authenticated but not permitted to perform the requested operation. In some applications, you may customize the response message or error page, but the default behavior is already sensible and secure.

Best Practices for Using Laravel Policies

To get the most benefit from policies, it helps to follow a few practical guidelines:

  • Keep controllers thin and move permission checks into policies
  • Do not mix business logic and authorization logic in the same method
  • Use policies for model-based rules and Gates for simple general checks
  • Keep policy methods small and easy to understand
  • Use meaningful custom methods for domain-specific actions
  • Test important authorization rules to avoid security mistakes
  • Do not rely only on Blade directives for protection

A good policy should be readable enough that another developer can quickly understand the permission rules for a resource.

Real World Example: Post Management Policy

Let us build a simple but realistic example. Suppose your application has blog posts. The rules are:

  • Any authenticated user can create a post
  • Users can update their own posts
  • Only administrators can delete any post

Here is a possible PostPolicy implementation:

<?php

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }

    public function view(User $user, Post $post): bool
    {
        return true;
    }

    public function create(User $user): bool
    {
        return true;
    }

    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->role === 'admin';
    }
}

Then the controller can stay simple:

public function destroy(Post $post)
{
    $this->authorize('delete', $post);

    $post->delete();

    return redirect()->route('posts.index')
        ->with('success', 'Post deleted successfully.');
}

This design is much cleaner than scattering role checks through every post-related endpoint.

Testing Laravel Policies

Authorization rules are important enough to deserve automated tests. Testing policies helps ensure your app does not accidentally allow or deny actions incorrectly.

You can test a policy directly or through feature tests. Direct policy tests are fast and focused.

<?php

namespace Tests\Unit;

use App\Models\Post;
use App\Models\User;
use App\Policies\PostPolicy;
use Tests\TestCase;

class PostPolicyTest extends TestCase
{
    public function test_owner_can_update_post(): void
    {
        $user = User::factory()->make(['id' => 1, 'role' => 'author']);
        $post = Post::factory()->make(['user_id' => 1]);

        $policy = new PostPolicy();

        $this->assertTrue($policy->update($user, $post));
    }

    public function test_non_owner_cannot_update_post(): void
    {
        $user = User::factory()->make(['id' => 2, 'role' => 'author']);
        $post = Post::factory()->make(['user_id' => 1]);

        $policy = new PostPolicy();

        $this->assertFalse($policy->update($user, $post));
    }

    public function test_admin_can_delete_any_post(): void
    {
        $user = User::factory()->make(['id' => 99, 'role' => 'admin']);
        $post = Post::factory()->make(['user_id' => 1]);

        $policy = new PostPolicy();

        $this->assertTrue($policy->delete($user, $post));
    }
}

You can also write feature tests to confirm protected routes return the correct responses.

public function test_user_cannot_delete_post_without_permission(): void
{
    $user = User::factory()->create(['role' => 'author']);
    $post = Post::factory()->create();

    $response = $this->actingAs($user)->delete(route('posts.destroy', $post));

    $response->assertForbidden();
}

These tests provide confidence that your authorization rules continue to work as the project evolves.

Summary

Laravel Policies are a clean and structured way to manage authorization in model-driven applications. They help you move permission logic out of controllers and into dedicated classes where it is easier to maintain, test, and reuse.

  • Authorization decides what a user can do, while authentication verifies who they are
  • Policies centralize model-based access rules
  • They keep controllers and views cleaner
  • Policies are better than scattered inline checks for long-term maintenance
  • They work well with controllers, Blade directives, and route middleware
  • Custom policy methods support real business actions like publish or approve
  • Automated tests help ensure security rules remain correct

Conclusion

If you are building a Laravel application with user-owned resources, roles, or permission-based actions, Policies should be one of your main tools. They provide a consistent pattern for authorization, improve code readability, and make your application easier to secure as it grows.

Instead of spreading checks across controllers and templates, define clear authorization rules in policy classes and reuse them everywhere. That approach leads to cleaner architecture, fewer mistakes, and a better developer experience. Mastering Laravel Policies is not just about writing cleaner code. It is also about building safer and more maintainable applications.

Mogail logo

Crafting digital experiences that inspire and engage. Let's build something amazing together.

Cairo, Egypt

Quick Links

Stay Updated

Subscribe to our newsletter for the latest updates, news, and exclusive content delivered straight to your inbox.

We respect your privacy. Unsubscribe anytime.

10,000+ subscribers

© 2026 Mogail. All rights reserved.