Important
This package is being renamed to avoid confusion with Laravel's own Guard/Gate terminology. Future development moves to custodian-laravel — same API, new name. New projects should start there; existing projects on guard-laravel keep working and can migrate later.
A powerful, flexible, and developer-friendly role and permission management system for Laravel applications.
Get up and running in 5 minutes:
Upgrading from an older version? Check the Upgrade Guide for detailed migration instructions.
composer require amdadulhaq/guard-laravelphp artisan vendor:publish --tag="guard-migrations"
php artisan migrate<?php
namespace App\Models;
use AmdadulHaq\Guard\Contracts\Roleable as RoleableContract;
use AmdadulHaq\Guard\Concerns\Roleable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements RoleableContract
{
use Roleable;
}php artisan guard:create-role admin Administrator
php artisan guard:create-permission users.create "Create Users"Route::middleware('role:admin')->get('/admin', [AdminController::class, 'index']);- Modern PHP & Laravel - Built for PHP 8.2+ and Laravel 11/12/13
- Flexible Permission System - Users can have permissions via roles
- Wildcard Permissions - Use
posts.*to match all post-related permissions - Real-Time Gate Integration - A single
Gate::beforehook resolves permissions and roles live; native@can,@canany,@cannotsupport with no stale definitions - Middleware Protection -
role,permission, androle_or_permissionmiddleware - Blade Directives -
@role,@hasrole,@hasanyrole,@hasallroles - Type-Safe Enums - IDE-friendly
PermissionTypeenum - Guarded Roles - Guarded roles cannot be deleted; attempts throw
GuardedRoleException - Permission Groups - Organize permissions by resource
- Interactive Commands - Laravel Prompts for creating roles/permissions
- Clean Architecture - Separated concerns with traits and contracts
- Developer Tools - Pint, Pest, Rector, and Larastan included
Building and maintaining high-quality open-source packages takes hundreds of hours of dedicated time. If you use Guard in your commercial applications or it saves you significant development time, please consider supporting the project.
Sponsor the Project Ensure the package stays actively maintained, receives rapid bug fixes, and continuous feature updates by becoming a monthly sponsor.
- Installation
- Upgrade Guide
- Configuration
- Usage
- Models Reference
- Exceptions
- Performance
- Database Structure
- Enums
- Development
- Troubleshooting
- FAQ
- PHP: 8.2, 8.3, 8.4, or 8.5
- Laravel: 11.x, 12.x, or 13.x
- Database: MySQL 5.7+, PostgreSQL 9.6+, SQLite 3.8+, or SQL Server 2017+
composer require amdadulhaq/guard-laravelphp artisan vendor:publish --tag="guard-migrations"
php artisan migrateThis creates 4 tables:
roles- Role definitionspermissions- Permission definitionspermission_role- Role-permission relationshipsrole_user- User-role relationships Pivot table names are derived from model table names; the defaults shown above are used unless you customize model tables.
<?php
namespace App\Models;
use AmdadulHaq\Guard\Contracts\Roleable as RoleableContract;
use AmdadulHaq\Guard\Concerns\Roleable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements RoleableContract
{
use Roleable;
}php artisan vendor:publish --tag="guard-config"The config/guard.php file:
return [
'models' => [
'user' => \App\Models\User::class,
'role' => \AmdadulHaq\Guard\Models\Role::class,
'permission' => \AmdadulHaq\Guard\Models\Permission::class,
],
'tables' => [
'roles' => 'roles',
'permissions' => 'permissions',
],
'middleware' => [
'role' => 'role',
'permission' => 'permission',
'role_or_permission' => 'role_or_permission',
],
'wildcard' => [
'enabled' => env('GUARD_WILDCARD_ENABLED', true),
],
];To extend or replace the default models, point the config at your own classes — all relations, commands, and gate checks resolve them from the config:
// config/guard.php
'models' => [
'user' => \App\Models\User::class,
'role' => \App\Models\Role::class, // extends AmdadulHaq\Guard\Models\Role
'permission' => \App\Models\Permission::class, // extends AmdadulHaq\Guard\Models\Permission
],Pivot table names are derived automatically from the models' table names.
Add the Roleable contract and trait to your user model:
use AmdadulHaq\Guard\Contracts\Roleable as RoleableContract;
use AmdadulHaq\Guard\Concerns\Roleable;
class User extends Authenticatable implements RoleableContract
{
use Roleable;
}Notes:
Roleabletrait on the user model handles both role and permission checks.- Users do not receive permissions directly.
- Assign permissions to roles, then users inherit them from those roles.
use AmdadulHaq\Guard\Models\Role;
// Set fields on create (or later via update())
$role = Role::create([
'name' => 'administrator', // required, unique — used by all checks
'label' => 'Administrator', // optional display name
'description' => 'Full system access',
'is_guarded' => true, // protect from deletion
]);
$role->update(['label' => 'Admin']);
// Get fields
$role->getName(); // 'administrator'
$role->getLabel(); // 'Administrator'
$role->getDescription(); // 'Full system access'
$role->isProtectedRole(); // true — deleting now throws GuardedRoleException
// Other role methods
$role->getPermissionNames(); // All permission names assigned to the role
$role->users; // Users with this role
// Query scopes
Role::guarded()->get(); // Only guarded roles
Role::unguarded()->get(); // Only unguarded rolesRoles can also be created via the CLI — see Artisan Commands.
use AmdadulHaq\Guard\Models\Permission;
// Set fields on create — only 'name' is required; the rest is display metadata
$permission = Permission::create([
'name' => 'users.delete', // required, unique — used by all checks
'label' => 'Delete Users', // optional display name
'description' => 'Permanently remove user accounts',
'group' => 'users', // optional stored grouping
]);
// Wildcard permission — is_wildcard is set automatically when name ends with '*'
Permission::create([
'name' => 'posts.*',
'label' => 'Manage All Posts',
'group' => 'posts',
]);
// Get fields
$permission->getName(); // 'users.delete'
$permission->getLabel(); // 'Delete Users'
$permission->getDescription(); // 'Permanently remove user accounts'
$permission->getGroup(); // 'users' (derived from the name prefix)
$permission->isWildcard(); // false
$permission->getType(); // PermissionType::DELETE (from the last name segment)
$permission->roles; // Roles with this permission
// Query scopes — group permissions for an admin UI
Permission::wildcard()->get(); // Only wildcard permissions
Permission::byGroup('users')->get(); // All users.* permissions
Permission::all()->groupBy->getGroup(); // ['users' => [...], 'posts' => [...]]Authorization only ever checks name — label, description, and group are display metadata for building admin UIs.
Note that getGroup() and byGroup() derive the group from the permission name prefix (users from users.create), not the stored group column, so the resource.action naming convention gives you grouping for free; the column is available for your own custom queries.
Permissions can also be created via the CLI — see Artisan Commands.
Wildcard permissions automatically match all sub-permissions:
// Create wildcard permission
Permission::create(['name' => 'posts.*']);
// Assign to role
$role->givePermissionTo('posts.*');
// Now user can do all of these:
$user->hasPermission('posts.create'); // true
$user->hasPermission('posts.update'); // true
$user->hasPermission('posts.delete'); // true
$user->hasPermission('posts.publish'); // trueThe is_wildcard boolean is automatically set when the name ends with *.
A permission named just * matches every permission — a super-admin grant. Wildcards can be disabled entirely via GUARD_WILDCARD_ENABLED=false.
Assigning Roles:
// Single role
$user->assignRole('administrator'); // by role name
$user->assignRole($roleModel); // by role model
// Multiple roles in one call
$user->assignRole('administrator', 'editor');
$user->assignRole([$roleModel, $roleId, 'moderator']);
// Sync (replaces all)
$user->syncRoles(['administrator', 'editor']);
$user->syncRoles([$role1->id, $role2->id]);
// Sync without detaching existing
$user->syncRolesWithoutDetaching(['moderator']);
// Revoke
$user->revokeRole('editor');
$user->revokeRole($roleModel);
$user->revokeRoles(); // Revoke allChecking Roles:
// Single role
$user->hasRole('administrator'); // true/false
// Multiple roles
$user->hasAllRoles(['admin', 'editor']); // Must have ALL
$user->hasAnyRole(['admin', 'moderator']); // Must have ANY
// Get role names
$user->getRoleNames(); // ['administrator', 'editor']
// Get role labels keyed by name (falls back to name when no label)
$user->getRoleLabels(); // ['administrator' => 'Administrator', 'editor' => 'editor']Assigning to Roles:
// Single permission
$role->givePermissionTo('users.create'); // by permission name
$role->givePermissionTo($permissionModel); // by permission model
// Multiple permissions in one call
$role->givePermissionTo('users.create', 'users.edit');
$role->givePermissionTo([$permissionModel, $permissionId, 'users.delete']);
// Sync (replaces all)
$role->syncPermissions(['users.create', 'users.edit']);
$role->syncPermissions([$perm1->id, $perm2->id]);
// Revoke
$role->revokePermissionTo('users.delete');
$role->revokePermissionTo($permissionModel);
$role->revokeAllPermissions();Checking Role Permissions:
$role->hasPermission('users.edit'); // Check if role has permission
$role->getPermissionNames(); // Get all permission namesChecking User Permissions:
// Check by name
$user->hasPermission('users.create');
// Check by model
$user->hasPermission($permissionModel);
// Wildcard matching
$user->hasPermission('posts.*');
// Get all permissions inherited from roles
$user->getPermissions();
// Get permission names array
$user->getPermissionNames(); // ['users.create', 'users.edit']Role Checking:
if ($user->hasRole('administrator')) {
// User has administrator role
}
if ($user->hasAllRoles(['admin', 'editor'])) {
// User has both roles
}
if ($user->hasAnyRole(['admin', 'moderator'])) {
// User has at least one role
}
// Get all role names
$user->getRoleNames(); // ['administrator', 'editor']Permission Checking:
if ($user->hasPermission('users.create')) {
// User can create users
}
if ($user->hasPermission('posts.*')) {
// User has wildcard permission for posts
}All middleware supports multiple values (requires ANY):
// Role middleware
Route::middleware('role:administrator')->get('/admin', [AdminController::class, 'index']);
// Multiple roles (requires ANY)
Route::middleware('role:admin,editor')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
// Permission middleware
Route::middleware('permission:users.create')->post('/users', [UserController::class, 'store']);
// Multiple permissions (requires ANY)
Route::middleware('permission:users.create,users.edit')->put('/users/{id}', [UserController::class, 'update']);
// Role OR permission middleware
Route::middleware('role_or_permission:admin,users.create')->get('/users', [UserController::class, 'index']);
// Multiple role_or_permission
Route::middleware('role_or_permission:admin,editor,posts.manage')->group(function () {
Route::post('/manage', [Controller::class, 'handle']);
});Responses: unauthenticated requests receive 401; authenticated users lacking access receive 403 (via PermissionDeniedException). Middleware aliases can be renamed in the middleware section of the config.
Guard registers a single Gate::before hook that resolves any ability as a permission or role at check time. New roles and permissions are usable immediately — no cache to clear, no gates to re-register. When the ability is not granted by Guard, the hook returns null, so your own Gate::define gates and policies still run as normal:
// In controllers
public function store(Request $request)
{
$this->authorize('users.create');
// User can create users
}
// Using Gate facade
use Illuminate\Support\Facades\Gate;
if (Gate::allows('users.create')) {
// Allowed
}
if (Gate::denies('users.delete')) {
abort(403, 'Permission denied');
}
// Check for specific user
if (Gate::forUser($otherUser)->allows('posts.edit')) {
// That user can edit posts
}
// Authorize roles
$this->authorize('administrator');Guard provides custom Blade directives for role checking, in addition to Laravel's built-in @can directives. All directives render nothing for guests — no need to wrap them in @auth:
Custom Role Directives:
@role('administrator')
<div class="admin-panel">
<h1>Admin Dashboard</h1>
</div>
@endrole
@hasrole('editor')
<p>Editor content here</p>
@endhasrole
@hasanyrole(['administrator', 'moderator'])
<p>Content for admins or moderators</p>
@endhasanyrole
@hasallroles(['administrator', 'editor'])
<p>Only for users with BOTH admin AND editor roles</p>
@endhasallrolesBuilt-in Laravel Directives (via Gate integration):
@can('users.create')
<a href="/users/create">Create User</a>
@endcan
@canany(['users.create', 'users.edit'])
<p>You can manage users</p>
@endcanany
@cannot('users.delete')
<p>You cannot delete users</p>
@endcannotCreate a Role:
php artisan guard:create-role admin Administrator
# Optionally assign it to a user by ID, email, or name
php artisan guard:create-role moderator "Moderator" 1
php artisan guard:create-role moderator "Moderator" user@example.com
php artisan guard:create-role moderator "Moderator" "Jane Doe"Create a Permission:
php artisan guard:create-permission users.create "Create Users"
# Optionally assign it to a role by ID or name
php artisan guard:create-permission users.delete "Delete Users" 1
php artisan guard:create-permission users.delete "Delete Users" adminBoth commands support Laravel Prompts when optional assignment arguments are omitted.
guard:create-roleprompts for an optional user identifier and accepts a user ID, email, or name.guard:create-permissionprompts for an optional role identifier and accepts a role ID or role name.
Upgrade helper:
php artisan guard:upgradeRewrites your application code from older Guard versions to the current architecture — updates v1/v2 trait and contract usage in app/Models, removes API calls deleted in v3, cleans the published config, and publishes any pending schema upgrade migrations. See the Upgrade Guide for details.
// Users with a specific role
User::query()->withRoles('administrator')->get();
// Users with a specific permission inherited through roles
User::query()->withPermissions('users.create')->get();
// Role scopes
Role::query()->guarded()->get();
Role::query()->unguarded()->get();
// Permission scopes
Permission::query()->wildcard()->get();
Permission::query()->byGroup('users')->get();User Model (via Traits)
Roleable trait provides:
roles()- BelongsToMany relationshipassignRole(...$roles)- Assign one or more rolessyncRoles(array $roles, bool $detach = true)- Sync rolessyncRolesWithoutDetaching(array $roles)- Sync without detachingrevokeRole($role)- Revoke specific rolerevokeRoles()- Revoke all rolesgetRoleNames()- Get all role namesgetRoleLabels()- Get role labels keyed by namehasRole($role)- Check single rolehasAllRoles(...$roles)- Check all roleshasAnyRole(...$roles)- Check any rolegetPermissionNames()- Get permission names inherited from roleshasPermission($permission)- Check permission (by name or model)getPermissions()- Get all permissions inherited from roles
Role Model
Properties:
name(string, unique)label(string, nullable)description(text, nullable)is_guarded(boolean)
Methods:
getName()- Get role nameisProtectedRole()- Check if guardedgetPermissionNames()- Get assigned permission namespermissions()- BelongsToMany to permissionsusers()- BelongsToMany to users
Scopes:
guarded()- Only guarded rolesunguarded()- Only unguarded roles
Permission Model
Properties:
name(string, unique)label(string, nullable)description(text, nullable)group(string, nullable, indexed)is_wildcard(boolean, auto-set)
Methods:
getName()- Get permission namegetLabel()- Get human-readable labelgetDescription()- Get descriptionisWildcard()- Check if wildcard patterngetGroup()- Get resource group (e.g., 'users')getType()- Get PermissionType enum from the name's last segment (null if not a known action)roles()- BelongsToMany to roles
Scopes:
wildcard()- Only wildcard permissionsbyGroup($group)- Filter by group
Guard Facade
Utility helpers used internally to derive table names — available if you need the same conventions (e.g. in your own migrations):
use AmdadulHaq\Guard\Facades\Guard;
Guard::getSingularName('roles'); // 'role'
Guard::getTableName(Role::class); // 'roles' (resolves the model's table)
Guard::getPivotTableName([Role::class, User::class]); // 'role_user' (alphabetical)Both models also expose getTable(), which resolves the table name from config('guard.tables.*').
use AmdadulHaq\Guard\Exceptions\PermissionDeniedException;
use AmdadulHaq\Guard\Exceptions\GuardedRoleException;
// Thrown by the middleware when a user lacks the required permission/role.
// Extends Symfony's HttpException, so it renders as an HTTP 403 response.
throw PermissionDeniedException::create('users.delete');
throw PermissionDeniedException::roleNotAssigned('administrator');
throw PermissionDeniedException::roleOrPermissionNotAssigned('admin, users.delete');
// Thrown when deleting a role with is_guarded = true
throw GuardedRoleException::cannotDelete('super-admin');Role and permission mutators (assignRole, givePermissionTo, syncRoles, revokeRole, ...) throw Illuminate\Database\Eloquent\ModelNotFoundException when a name does not resolve to an existing model — typos fail loudly instead of silently doing nothing.
Permission checks are memoized per model instance, so repeated hasPermission() calls within a request hit the database once. Role checks use the loaded roles relation, which Guard refreshes automatically after any role mutation.
Roles Table
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('label')->nullable();
$table->text('description')->nullable();
$table->boolean('is_guarded')->default(false);
$table->timestamps();
});Permissions Table
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('label')->nullable();
$table->text('description')->nullable();
$table->string('group')->nullable()->index();
$table->boolean('is_wildcard')->default(false);
$table->timestamps();
});Permission-Role Pivot
Schema::create('permission_role', function (Blueprint $table) {
$table->foreignId('permission_id')->constrained()->cascadeOnDelete();
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->primary(['permission_id', 'role_id']);
});Role-User Pivot
Schema::create('role_user', function (Blueprint $table) {
$table->foreignId('role_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->primary(['role_id', 'user_id']);
});use AmdadulHaq\Guard\Enums\PermissionType;
PermissionType::CREATE->label(); // "Create"
PermissionType::READ->label(); // "Read"
PermissionType::WRITE->label(); // "Write"
PermissionType::UPDATE->label(); // "Update"
PermissionType::DELETE->label(); // "Delete"
PermissionType::VIEW_ANY->label(); // "View any"
PermissionType::VIEW->label(); // "View"
PermissionType::RESTORE->label(); // "Restore"
PermissionType::FORCE_DELETE->label(); // "Force delete"
PermissionType::MANAGE->label(); // "Manage"# Rector (code refactoring)
composer refactor
composer refactor:check
# Laravel Pint (code style)
composer lint
composer lint:check
# Pest (testing)
composer test
composer test-coverage
# Larastan (static analysis)
composer analyseClass 'AmdadulHaq\Guard\Concerns\Roleable' not found
Solution:
composer dump-autoloadTarget class [role] does not exist.
Solution:
php artisan config:clearPermissions not being recognized
Permissions are resolved live from the database — make sure the permission exists, is assigned to one of the user's roles, and that you're checking a fresh model instance ($user->fresh()) if roles were changed on a different instance.
-
Use wildcard permissions to reduce permission count
-
Filter at database level instead of loading all users:
// Good User::whereHas('roles', fn ($q) => $q->where('name', 'admin'))->get(); // Less efficient User::all()->filter(fn ($u) => $u->hasRole('admin'));
-
Eager load when needed:
User::with(['roles', 'roles.permissions'])->get();
Can I use this with Laravel Sanctum?
Yes! Guard works seamlessly with Sanctum and any auth system.
Can users have permissions without roles?
No, users receive permissions via roles.
How do wildcard permissions work?
Create a permission like posts.* and it automatically matches posts.create, posts.edit, etc.
Can I customize table names?
Yes, publish the config and modify the tables section.
Does it work with multiple guards?
Yes, it integrates with Laravel's authorization system.
Is there a UI for managing roles?
Guard is backend-only. For a UI, consider Filament Shield or build your own.
What Blade directives does Guard provide?
Guard ships with @role, @hasrole, @hasanyrole, and @hasallroles. Laravel's built-in @can, @canany, and @cannot also work through Gate integration.
Can permissions be assigned to permissions?
No, permissions are assigned to roles.
We welcome contributions! Please see CONTRIBUTING for details.
See CHANGELOG for recent changes.
Please review our security policy for reporting vulnerabilities.
If Guard helps you, a star helps the project grow.
The MIT License (MIT). See License File for details.
Made with ❤️ for the Laravel community