Role and Permission Management in Laravel 12 using Policies & Gates (Step-by-Step)

Author

Kritim Yantra

Jun 03, 2025

Role and Permission Management in Laravel 12 using Policies & Gates (Step-by-Step)

Controlling who can access what is essential for any web application. Whether you're building a blog, admin panel, or a SaaS platform—Role-Based Access Control (RBAC) ensures users only do what they're allowed to.

Laravel 12 makes this easy with two built-in tools:

  • Gates – for general access rules
  • Policies – for model-specific rules

In this guide, you’ll learn how to implement a full role and permission system using Laravel’s Gates and Policies. We’ll keep it simple, clean, and practical 💡


🧠 Gates vs Policies – What's the Difference?

Before we start coding, let’s understand these tools:

Feature Gates Policies
Type Closure-based Class-based
Best For General checks (e.g. "access-dashboard") Model-specific checks (e.g. "edit-post")

Use Gates: When the action is general, like accessing an admin panel
Use Policies: When the action depends on a specific model, like editing a post


🛠️ Step 1: Set Up Database Tables

We’ll need tables for roles, permissions, and their relationships.

// Roles table
Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->string('description')->nullable();
    $table->timestamps();
});

// Permissions table
Schema::create('permissions', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->string('description')->nullable();
    $table->timestamps();
});

// Pivot table to link roles and permissions
Schema::create('role_permission', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained()->cascadeOnDelete();
    $table->foreignId('permission_id')->constrained()->cascadeOnDelete();
    $table->primary(['role_id', 'permission_id']);
});

// Add role to users
Schema::table('users', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained()->default(3); // default = 'user'
});

🧩 Step 2: Define Relationships in Models

// app/Models/Role.php
class Role extends Model {
    public function users() {
        return $this->hasMany(User::class);
    }

    public function permissions() {
        return $this->belongsToMany(Permission::class);
    }
}

// app/Models/Permission.php
class Permission extends Model {
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

// app/Models/User.php
class User extends Authenticatable {
    public function role() {
        return $this->belongsTo(Role::class);
    }

    public function hasPermission($name) {
        return $this->role->permissions->contains('name', $name);
    }
}

🔐 Step 3: Working with Gates

📌 Define Gates in AuthServiceProvider

use Illuminate\Support\Facades\Gate;

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

    // Dynamic Gates from DB
    foreach (Permission::all() as $permission) {
        Gate::define($permission->name, function ($user) use ($permission) {
            return $user->hasPermission($permission->name);
        });
    }

    // Or define manually
    Gate::define('access-admin-dashboard', fn($user) => $user->role->name === 'admin');
}

✅ Using Gates

// In controller
if (Gate::allows('edit-settings')) {
    // Allow editing
}

// In Blade
@can('edit-settings')
    <a href="/settings/edit">Edit Settings</a>
@endcan

🧭 Step 4: Using Policies for Models

🔧 Create a Policy

php artisan make:policy PostPolicy --model=Post

✏️ Define Permission Logic

// app/Policies/PostPolicy.php
class PostPolicy {
    public function view(User $user, Post $post) {
        return $user->hasPermission('view-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('view-own-posts'));
    }

    public function create(User $user) {
        return $user->hasPermission('create-posts');
    }

    public function update(User $user, Post $post) {
        return $user->hasPermission('update-any-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('update-own-posts'));
    }

    public function delete(User $user, Post $post) {
        return $user->hasPermission('delete-any-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('delete-own-posts'));
    }
}

🗂️ Register the Policy

protected $policies = [
    Post::class => PostPolicy::class,
];

🧪 Use Policies in Controllers & Views

// Controller
$this->authorize('update', $post);

// Blade
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

️ Advanced Usage

✅ Use Middleware

Route::post('/post', function () {
    // Create post logic
})->middleware('can:create,App\Models\Post');

✅ Blade Helpers

@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}">New Post</a>
@endcan

@cannot('delete', $post)
    You can't delete this post.
@endcannot

🚀 Boost Performance: Cache Permissions

// User model
public function getPermissionsAttribute() {
    return Cache::remember("user.{$this->id}.permissions", now()->addDay(), function () {
        return $this->role->permissions->pluck('name')->toArray();
    });
}

public function hasPermission($name) {
    return in_array($name, $this->permissions);
}

🧪 Testing Authorization

// tests/Feature/PostPolicyTest.php

public function test_admin_can_update_any_post() {
    $admin = User::factory()->create(['role_id' => Role::where('name', 'admin')->first()->id]);
    $post = Post::factory()->create();
    $this->assertTrue($admin->can('update', $post));
}

public function test_user_cannot_delete_others_posts() {
    $user1 = User::factory()->create();
    $user2 = User::factory()->create();
    $post = Post::factory()->create(['user_id' => $user2->id]);
    $this->assertFalse($user1->can('delete', $post));
}

🛠️ Bonus: Manage Permissions via Artisan

// app/Console/Commands/CreatePermission.php
protected $signature = 'permission:create {name} {description?}';

public function handle() {
    $permission = Permission::create([
        'name' => $this->argument('name'),
        'description' => $this->argument('description') ?? '',
    ]);
    $this->info("Permission {$permission->name} created!");
}

🧭 Best Practices

  • ✅ Use "verb-noun" naming for permissions (edit-user, delete-post)
  • ✅ Prefer policies over gates for model actions
  • ✅ Keep logic inside policies, not scattered in controllers
  • ✅ Cache permissions to reduce DB hits
  • ✅ Regularly test all permission flows

️ Common Mistakes to Avoid

  • ❌ Using gates for model-specific logic
  • ❌ Forgetting to register policies
  • ❌ Not caching permissions = performance hit
  • ❌ Hardcoding roles instead of using permissions
  • ❌ Not testing edge cases

✅ Conclusion

Laravel makes it easy to build secure, maintainable role and permission systems using Gates and Policies. By following the steps above, you can:

  • Grant fine-grained access
  • Keep your code clean
  • Easily expand as your app grows

🔐 Authorization isn’t just a feature—it’s a security backbone. So take time to plan, test, and document your permissions well.

Happy coding! 🚀

LIVE MENTORSHIP ONLY 5 SPOTS

Laravel Mastery
Coaching Class Program

KritiMyantra

Transform from beginner to Laravel expert with our personalized Coaching Class starting June 20, 2025. Limited enrollment ensures focused attention.

Daily Sessions

1-hour personalized coaching

Real Projects

Build portfolio applications

Best Practices

Industry-standard techniques

Career Support

Interview prep & job guidance

Total Investment
$200
Duration
30 hours
1h/day

Enrollment Closes In

Days
Hours
Minutes
Seconds
Spots Available 5 of 10 remaining
Next cohort starts:
June 20, 2025

Join the Program

Complete your application to secure your spot

Application Submitted!

Thank you for your interest in our Laravel mentorship program. We'll contact you within 24 hours with next steps.

What happens next?

  • Confirmation email with program details
  • WhatsApp message from our team
  • Onboarding call to discuss your goals

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts