Laravel Permission Development
When to use this skill
Use this skill when working with authorization, roles, permissions, access control, middleware guards, or Blade permission directives using spatie/laravel-permission.
Core Concepts
-
Users have Roles, Roles have Permissions, Apps check Permissions (not Roles).
-
Direct permissions on users are an anti-pattern; assign permissions to roles instead.
-
Use $user->can('permission-name') for all authorization checks (supports Super Admin via Gate).
-
The HasRoles trait (which includes HasPermissions ) is added to User models.
Setup
Add the HasRoles trait to your User model:
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable { use HasRoles; }
Creating Roles and Permissions
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Permission;
$role = Role::create(['name' => 'writer']); $permission = Permission::create(['name' => 'edit articles']);
// findOrCreate is idempotent (safe for seeders) $role = Role::findOrCreate('writer', 'web'); $permission = Permission::findOrCreate('edit articles', 'web');
Assigning Roles and Permissions
// Assign roles to users $user->assignRole('writer'); $user->assignRole('writer', 'admin'); $user->assignRole(['writer', 'admin']); $user->syncRoles(['writer', 'admin']); // replaces all $user->removeRole('writer');
// Assign permissions to roles (preferred) $role->givePermissionTo('edit articles'); $role->givePermissionTo(['edit articles', 'delete articles']); $role->syncPermissions(['edit articles', 'delete articles']); $role->revokePermissionTo('edit articles');
// Reverse assignment $permission->assignRole('writer'); $permission->syncRoles(['writer', 'editor']); $permission->removeRole('writer');
Checking Roles and Permissions
// Permission checks (preferred - supports Super Admin via Gate) $user->can('edit articles'); $user->canAny(['edit articles', 'delete articles']);
// Direct package methods (bypass Gate, no Super Admin support) $user->hasPermissionTo('edit articles'); $user->hasAnyPermission(['edit articles', 'publish articles']); $user->hasAllPermissions(['edit articles', 'publish articles']); $user->hasDirectPermission('edit articles');
// Role checks $user->hasRole('writer'); $user->hasAnyRole(['writer', 'editor']); $user->hasAllRoles(['writer', 'editor']); $user->hasExactRoles(['writer', 'editor']);
// Get assigned roles and permissions $user->getRoleNames(); // Collection of role name strings $user->getPermissionNames(); // Collection of permission name strings $user->getDirectPermissions(); // Direct permissions only $user->getPermissionsViaRoles(); // Inherited via roles $user->getAllPermissions(); // Both direct and inherited
Query Scopes
$users = User::role('writer')->get(); $users = User::withoutRole('writer')->get(); $users = User::permission('edit articles')->get(); $users = User::withoutPermission('edit articles')->get();
Middleware
Register middleware aliases in bootstrap/app.php :
->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, ]); })
Use in routes (pipe | for OR logic):
Route::middleware(['permission:edit articles'])->group(function () { ... }); Route::middleware(['role:manager|writer'])->group(function () { ... }); Route::middleware(['role_or_permission:manager|edit articles'])->group(function () { ... });
// With specific guard Route::middleware(['role:manager,api'])->group(function () { ... });
For single permissions, Laravel's built-in can middleware also works:
Route::middleware(['can:edit articles'])->group(function () { ... });
Blade Directives
Prefer @can (permission-based) over @role (role-based):
@can('edit articles') {{-- User can edit articles (supports Super Admin) --}} @endcan
@canany(['edit articles', 'delete articles']) {{-- User can do at least one --}} @endcanany
@role('admin') {{-- Only use for super-admin type checks --}} @endrole
@hasanyrole('writer|admin') {{-- Has writer or admin --}} @endhasanyrole
Super Admin
Use Gate::before in AppServiceProvider::boot() :
use Illuminate\Support\Facades\Gate;
public function boot(): void { Gate::before(function ($user, $ability) { return $user->hasRole('Super Admin') ? true : null; }); }
This makes $user->can() and @can always return true for Super Admins. Must return null (not false ) to allow normal checks for other users.
Policies
Use $user->can() inside policy methods to check permissions:
class PostPolicy { public function update(User $user, Post $post): bool { if ($user->can('edit all posts')) { return true; }
return $user->can('edit own posts') && $user->id === $post->user_id;
}
}
Enums
enum RolesEnum: string { case WRITER = 'writer'; case EDITOR = 'editor'; }
enum PermissionsEnum: string { case EDIT_POSTS = 'edit posts'; case DELETE_POSTS = 'delete posts'; }
// Creation requires ->value Permission::findOrCreate(PermissionsEnum::EDIT_POSTS->value, 'web');
// Most methods accept enums directly $user->assignRole(RolesEnum::WRITER); $user->hasRole(RolesEnum::WRITER); $role->givePermissionTo(PermissionsEnum::EDIT_POSTS); $user->hasPermissionTo(PermissionsEnum::EDIT_POSTS);
Seeding
Always flush the permission cache when seeding:
class RolesAndPermissionsSeeder extends Seeder { public function run(): void { // Reset cache app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
Permission::findOrCreate('edit articles', 'web');
Permission::findOrCreate('delete articles', 'web');
// Create roles and assign permissions
Role::findOrCreate('writer', 'web')
->givePermissionTo(['edit articles']);
Role::findOrCreate('admin', 'web')
->givePermissionTo(Permission::all());
}
}
Teams (Multi-Tenancy)
Enable in config/permission.php before running migrations:
'teams' => true,
Set the active team in middleware:
setPermissionsTeamId($teamId);
When switching teams, unset cached relations:
$user->unsetRelation('roles')->unsetRelation('permissions');
Events
Enable in config/permission.php :
'events_enabled' => true,
Available events: RoleAttachedEvent , RoleDetachedEvent , PermissionAttachedEvent , PermissionDetachedEvent in the Spatie\Permission\Events namespace.
Performance
-
Permissions are cached automatically. The cache is flushed when roles/permissions change via package methods.
-
After direct DB operations, flush manually: app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions()
-
For bulk seeding, use Permission::insert() for speed, but flush the cache afterward.