enadstack / laravel-roles
Reusable Roles & Permissions (single or multi-tenant teams) on spatie/laravel-permission
Installs: 26
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/enadstack/laravel-roles
Requires
- php: >=8.2
- illuminate/support: ^12.0
- spatie/laravel-permission: ^6.0
Requires (Dev)
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- phpunit/phpunit: ^11.5
Suggests
- stancl/tenancy: Needed only when using multi-database tenancy mode
README
A complete, production-ready Laravel package for managing roles and permissions with a clean API, service layer architecture, and 100% test coverage. Built on top of Spatie Laravel Permission.
๐ Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- API Endpoints
- Usage Scenarios
- Service Layer
- Events
- Multi-Tenancy
- Testing
- Troubleshooting
- Contributing
โจ Features
Core Features
- โ Complete REST API for roles and permissions management
- โ Service Layer Architecture - Clean, testable business logic
- โ 100% Test Coverage - 32 passing integration tests
- โ FormRequest Validation - Centralized validation rules
- โ API Resources - Consistent JSON responses
- โ Domain Events - 6 event types for extensibility
Role Management
- Create, read, update, delete roles
- Soft deletes with restore functionality
- Force delete (permanent deletion)
- Bulk operations (delete, restore, force delete)
- Clone roles with permissions
- Assign/sync permissions to roles
- Add/remove single permissions
- Role statistics and recent roles
Permission Management
- Create, read, update, delete permissions
- Soft deletes with restore
- Permission grouping (e.g., users, posts, offers)
- Search and filter by group
- Permission statistics
- Recent permissions
- Permission Matrix - Visual grid of roles ร permissions
- Grouped permissions view
Advanced Features
- Multi-tenancy support (single, team-scoped, multi-database)
- i18n support - Multi-language labels and descriptions
- Config-driven permission seeding
- Sync command for CI/CD deployments
- Cache management with auto-invalidation
- Middleware for team-scoped tenancy
- Current user ACL endpoints
๐ง Requirements
- PHP >= 8.2
- Laravel >= 12.0
- Spatie Laravel Permission ^6.0
๐ฆ Installation
Step 1: Install via Composer
composer require enadstack/laravel-roles
Step 2: Run the Interactive Installer (First Time Only!)
php artisan roles:install
โ ๏ธ IMPORTANT: Only run
php artisan roles:installduring initial installation. When upgrading from an older version (e.g.,composer update), do NOT re-run this command as it will ask to reconfigure yourconfig/roles.phpfile. Your existing configuration will be preserved automatically during upgrades.
The installer will:
- โ Publish Spatie Permission config and migrations
- โ
Publish package config (
config/roles.php) - โ Ask if you want multi-language support (i18n)
- โ Ask which tenancy mode (single/team-scoped/multi-database)
- โ Run migrations
- โ Optionally seed initial roles & permissions
Installer Options:
# Install with seeds automatically php artisan roles:install --with-seeds # Specify custom team key for team-scoped mode php artisan roles:install --team-key=tenant_id
Step 3: Configure (Optional)
Edit config/roles.php to customize settings:
return [ // Multi-language support 'i18n' => [ 'enabled' => false, // Set to true for multi-language 'locales' => ['en', 'ar'], 'default' => 'en', ], 'guard' => 'web', // Tenancy mode 'tenancy' => [ 'mode' => 'single', // 'single' | 'team_scoped' | 'multi_database' 'team_foreign_key' => 'team_id', ], // API Routes 'routes' => [ 'prefix' => 'admin/acl', 'middleware' => ['api', 'auth:sanctum'], 'expose_me' => true, // /me/roles, /me/permissions endpoints ], // Cache 'cache' => [ 'enabled' => true, 'ttl' => 300, // seconds ], ];
๐ Upgrading from Previous Versions
Upgrading to v1.1.0 from v1.0.x
# Update the package composer update enadstack/laravel-roles # Clear caches php artisan config:clear php artisan cache:clear php artisan permission:cache-reset
โ That's it! No additional steps needed. Your
config/roles.phpwill be preserved. Do NOT runphp artisan roles:installagain unless you want to reconfigure.
What Changes in v1.1.0
- โ Enhanced documentation (no code changes)
- โ All existing functionality works exactly the same
- โ No breaking changes
- โ Your config file is preserved
If You Accidentally Ran roles:install
If you accidentally ran php artisan roles:install and your config was modified:
-
Restore from Git (if you have version control):
git checkout config/roles.php
-
Or manually republish the original config:
php artisan vendor:publish --tag=roles-config --force # Then reconfigure your settings in config/roles.php
๐ Quick Start
1. Create a Role
POST /admin/acl/roles
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
{
"name": "editor",
"label": {"en": "Content Editor", "ar": "ู
ุญุฑุฑ ุงูู
ุญุชูู"},
"description": {"en": "Can edit content"}
}
Response:
{
"data": {
"id": 1,
"name": "editor",
"guard_name": "web",
"label": {"en": "Content Editor"},
"description": {"en": "Can edit content"},
"created_at": "2025-11-15T10:00:00Z"
}
}
2. Create Permissions
POST /admin/acl/permissions
Content-Type: application/json
{
"name": "posts.create",
"group": "posts",
"label": {"en": "Create Posts"}
}
3. Assign Permissions to Role
POST /admin/acl/roles/1/permissions
Content-Type: application/json
{
"permission_ids": [1, 2, 3]
}
4. View Permission Matrix
GET /admin/acl/permissions-matrix
Response:
{
"data": {
"roles": [
{"id": 1, "name": "editor"}
],
"matrix": [
{
"permission_id": 1,
"permission_name": "posts.create",
"roles": {
"1": true
}
}
],
"generated_at": "2025-11-15T10:00:00Z"
}
}
โ๏ธ Configuration
Basic Setup
After installation, your config/roles.php will contain all available options:
return [ // Enable/disable i18n (multi-language) support 'i18n' => [ 'enabled' => false, 'locales' => ['en', 'ar'], 'default' => 'en', ], // Default guard for roles/permissions 'guard' => 'web', // Tenancy configuration 'tenancy' => [ 'mode' => 'single', // 'single' | 'team_scoped' | 'multi_database' 'team_foreign_key' => 'team_id', 'provider' => null, // e.g., 'stancl/tenancy' ], // API route configuration 'routes' => [ 'prefix' => 'admin/acl', 'middleware' => ['api', 'auth:sanctum'], 'expose_me' => true, // Enable /me endpoints ], // Cache settings 'cache' => [ 'enabled' => true, 'ttl' => 300, // 5 minutes 'keys' => [ 'permission_matrix' => 'laravel_roles.permission_matrix', 'grouped_permissions' => 'laravel_roles.grouped_permissions', ], ], // Seed configuration (for roles:sync command) 'seed' => [ 'roles' => ['super-admin', 'admin', 'manager'], 'permission_groups' => [ 'users' => ['list', 'create', 'show', 'update', 'delete'], 'roles' => ['list', 'create', 'show', 'update', 'delete'], 'permissions' => ['list', 'show'], ], 'map' => [ 'super-admin' => ['*'], // All permissions 'admin' => ['users.*', 'roles.*'], 'manager' => ['users.list', 'users.show'], ], ], ];
๐ก API Endpoints
Roles
| Method | Endpoint | Description |
|---|---|---|
| GET | /admin/acl/roles |
List all roles (paginated) |
| POST | /admin/acl/roles |
Create a new role |
| GET | /admin/acl/roles/{id} |
Show single role |
| PUT | /admin/acl/roles/{id} |
Update role |
| DELETE | /admin/acl/roles/{id} |
Soft delete role |
| POST | /admin/acl/roles/{id}/restore |
Restore soft-deleted role |
| DELETE | /admin/acl/roles/{id}/force |
Force delete role (permanent) |
| POST | /admin/acl/roles/bulk-delete |
Bulk soft delete {ids: []} |
| POST | /admin/acl/roles/bulk-restore |
Bulk restore {ids: []} |
| POST | /admin/acl/roles/bulk-force-delete |
Bulk force delete {ids: []} |
| GET | /admin/acl/roles-recent?limit=10 |
Get recent roles |
| GET | /admin/acl/roles-stats |
Get role statistics |
| POST | /admin/acl/roles/{id}/permissions |
Assign permissions {permission_ids: []} |
| GET | /admin/acl/roles/{id}/permissions |
Get role's permissions |
| GET | /admin/acl/roles-permissions |
Permissions grouped by role |
| POST | /admin/acl/roles/{role}/permission |
Add single permission {permission_id: X} |
| DELETE | /admin/acl/roles/{role}/permission |
Remove single permission {permission_id: X} |
| POST | /admin/acl/roles/{role}/clone |
Clone role {name: "new-name"} |
Permissions
| Method | Endpoint | Description |
|---|---|---|
| GET | /admin/acl/permissions?group=users |
List permissions (filterable, searchable) |
| POST | /admin/acl/permissions |
Create permission |
| GET | /admin/acl/permissions/{id} |
Show permission |
| PUT | /admin/acl/permissions/{id} |
Update permission |
| DELETE | /admin/acl/permissions/{id} |
Soft delete permission |
| POST | /admin/acl/permissions/{id}/restore |
Restore permission |
| DELETE | /admin/acl/permissions/{id}/force |
Force delete permission |
| POST | /admin/acl/permissions/bulk-force-delete |
Bulk force delete {ids: []} |
| GET | /admin/acl/permissions-recent?limit=10 |
Recent permissions |
| GET | /admin/acl/permissions-stats |
Permission statistics |
| GET | /admin/acl/permission-groups |
Grouped permissions |
| GET | /admin/acl/permissions-matrix |
Permission matrix |
Current User (if expose_me is true)
| Method | Endpoint | Description |
|---|---|---|
| GET | /admin/acl/me/roles |
Current user's roles |
| GET | /admin/acl/me/permissions |
Current user's permissions |
| GET | /admin/acl/me/abilities |
Current user's abilities |
๐ก Usage Scenarios
Scenario 1: Setup for a Blog Platform
# 1. Install the package composer require enadstack/laravel-roles php artisan roles:install # 2. Define permissions in config/roles.php 'seed' => [ 'permission_groups' => [ 'posts' => ['list', 'create', 'update', 'delete', 'publish'], 'comments' => ['list', 'create', 'update', 'delete', 'moderate'], 'categories' => ['list', 'create', 'update', 'delete'], ], 'map' => [ 'admin' => ['*'], 'editor' => ['posts.*', 'categories.*'], 'author' => ['posts.create', 'posts.update', 'posts.list'], 'moderator' => ['comments.*', 'posts.list'], ], ], # 3. Sync permissions php artisan roles:sync # 4. Assign roles to users (in your code) $user->assignRole('editor');
Scenario 2: E-commerce Platform
# config/roles.php 'seed' => [ 'permission_groups' => [ 'products' => ['list', 'create', 'update', 'delete', 'import', 'export'], 'orders' => ['list', 'show', 'update', 'cancel', 'refund'], 'customers' => ['list', 'show', 'update', 'delete'], 'reports' => ['sales', 'inventory', 'customers'], ], 'map' => [ 'super-admin' => ['*'], 'store-manager' => ['products.*', 'orders.*', 'reports.*'], 'sales-rep' => ['orders.list', 'orders.show', 'orders.update', 'customers.list'], 'inventory-manager' => ['products.list', 'products.update', 'reports.inventory'], ], ],
Scenario 3: SaaS Application (Multi-Tenancy)
# 1. Configure team-scoped tenancy # config/roles.php 'tenancy' => [ 'mode' => 'team_scoped', 'team_foreign_key' => 'tenant_id', ], # 2. Add middleware to routes Route::middleware(['auth:sanctum', 'set.tenant'])->group(function () { // Your routes here }); # 3. Sync permissions per tenant php artisan roles:sync --team-id=123 # 4. Use in your code setPermissionsTeamId($tenantId); $user->assignRole('tenant-admin');
Scenario 4: Adding New Module Permissions
When you add a new feature (e.g., "Offers" module):
Step 1: Update config/roles.php
'seed' => [ 'permission_groups' => [ // ...existing groups... 'offers' => ['list', 'create', 'update', 'delete', 'approve', 'reject'], ], 'map' => [ 'admin' => ['offers.*'], 'offer-manager' => ['offers.*'], 'sales-rep' => ['offers.list', 'offers.create'], ], ],
Step 2: Sync permissions
php artisan roles:sync
Step 3: Clear cache
php artisan permission:cache-reset
Done! Your new permissions are now available.
๐ฏ Service Layer
Using RoleService
use Enadstack\LaravelRoles\Services\RoleService; class YourController extends Controller { public function __construct(protected RoleService $roleService) {} public function index() { // List roles with pagination and search $roles = $this->roleService->list([ 'search' => 'admin', 'per_page' => 20, 'sort_by' => 'name', 'sort_order' => 'asc', 'with_trashed' => false, ]); return response()->json($roles); } public function store(Request $request) { // Create role $role = $this->roleService->create([ 'name' => 'content-manager', 'guard_name' => 'web', 'label' => ['en' => 'Content Manager'], 'description' => ['en' => 'Manages all content'], ]); return response()->json($role, 201); } public function update(Role $role, Request $request) { // Update role $updated = $this->roleService->update($role, $request->validated()); return response()->json($updated); } public function assignPermissions(Role $role, Request $request) { // Assign permissions to role $role = $this->roleService->assignPermissions($role, $request->permission_ids); return response()->json(['message' => 'Permissions assigned successfully']); } public function stats() { // Get statistics $stats = $this->roleService->stats(); // Returns: // [ // 'total' => 10, // 'active' => 8, // 'deleted' => 2, // 'with_permissions' => 7, // 'without_permissions' => 3, // ] return response()->json($stats); } public function clone(Role $role, Request $request) { // Clone role with all permissions $cloned = $this->roleService->cloneWithPermissions($role, $request->name); return response()->json($cloned, 201); } }
Using PermissionService
use Enadstack\LaravelRoles\Services\PermissionService; class PermissionController extends Controller { public function __construct(protected PermissionService $permissionService) {} public function index(Request $request) { // List with filters $permissions = $this->permissionService->list([ 'search' => 'user', 'group' => 'users', 'per_page' => 50, ]); return response()->json($permissions); } public function matrix() { // Get permission matrix (cached) $matrix = $this->permissionService->getPermissionMatrix(); // Returns: // [ // 'roles' => [...], // 'matrix' => [ // ['permission_id' => 1, 'permission_name' => 'users.create', 'roles' => ['1' => true, '2' => false]], // ... // ] // ] return response()->json($matrix); } public function grouped() { // Get grouped permissions (cached) $grouped = $this->permissionService->getGroupedPermissions(); // Returns: // [ // 'users' => ['label' => 'Users', 'permissions' => [...]], // 'posts' => ['label' => 'Posts', 'permissions' => [...]], // ] return response()->json($grouped); } public function stats() { $stats = $this->permissionService->stats(); // Returns: // [ // 'total' => 50, // 'active' => 45, // 'deleted' => 5, // 'assigned' => 40, // 'unassigned' => 10, // 'by_group' => ['users' => 10, 'posts' => 8, ...], // ] return response()->json($stats); } }
๐ Events
The package dispatches 6 domain events that you can listen to:
Available Events
use Enadstack\LaravelRoles\Events\RoleCreated; use Enadstack\LaravelRoles\Events\RoleUpdated; use Enadstack\LaravelRoles\Events\RoleDeleted; use Enadstack\LaravelRoles\Events\PermissionCreated; use Enadstack\LaravelRoles\Events\PermissionUpdated; use Enadstack\LaravelRoles\Events\PermissionsAssignedToRole;
Listening to Events
In EventServiceProvider:
protected $listen = [ RoleCreated::class => [ SendRoleCreatedNotification::class, LogRoleCreation::class, ], RoleDeleted::class => [ NotifyAdminsOfRoleDeletion::class, ], PermissionsAssignedToRole::class => [ ClearPermissionCache::class, LogPermissionAssignment::class, ], ];
Event Listener Example:
namespace App\Listeners; use Enadstack\LaravelRoles\Events\RoleCreated; use Illuminate\Support\Facades\Log; class LogRoleCreation { public function handle(RoleCreated $event): void { Log::info('New role created', [ 'role_id' => $event->role->id, 'role_name' => $event->role->name, 'created_by' => auth()->id(), ]); // Send notification, update audit log, etc. } }
๐ข Multi-Tenancy
Single Tenant (Default)
No additional setup required. All roles and permissions are shared across the application.
Team-Scoped Tenancy
Setup:
- Configure in
config/roles.php:
'tenancy' => [ 'mode' => 'team_scoped', 'team_foreign_key' => 'tenant_id', // or 'team_id', 'organization_id' ],
- Add middleware to routes:
use Enadstack\LaravelRoles\Http\Middleware\SetPermissionTeamId; // In bootstrap/app.php or Http/Kernel.php ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'set.tenant' => SetPermissionTeamId::class, ]); }) // Apply to routes Route::middleware(['auth:sanctum', 'set.tenant'])->group(function () { Route::get('/admin/acl/roles', [RoleController::class, 'index']); });
- Sync permissions per tenant:
php artisan roles:sync --team-id=123
In Your Code:
// Set team context setPermissionsTeamId($tenantId); // Or manually app()->instance('permission.team_id', $tenantId); // Now all operations are scoped to this tenant $user->assignRole('admin'); // Only within this tenant $user->hasRole('admin'); // Checks within this tenant
Multi-Database Tenancy
Setup:
- Configure:
'tenancy' => [ 'mode' => 'multi_database', 'provider' => 'stancl/tenancy', // Your tenancy provider ],
- Run migrations per tenant:
# Using Stancl Tenancy php artisan tenants:artisan "migrate --path=database/migrations" # Sync permissions per tenant php artisan tenants:artisan "roles:sync"
๐งช Testing
Running Tests
# Run all tests ./vendor/bin/pest # Run specific test file ./vendor/bin/pest tests/Feature/RoleApiTest.php # Run with coverage ./vendor/bin/pest --coverage
Test Coverage
The package includes 32 comprehensive integration tests:
- โ Role CRUD operations (14 tests)
- โ Permission CRUD operations (14 tests)
- โ Permission matrix (1 test)
- โ Sync command (2 tests)
- โ Bulk operations (1 test)
All tests passing: 32/32 (100%)
๐ Troubleshooting
Cache Issues
Problem: Changes not reflecting immediately
Solution:
php artisan permission:cache-reset php artisan cache:clear
Migration Errors
Problem: "Table already exists" error
Solution:
# Check if migrations already ran php artisan migrate:status # If needed, rollback and re-run php artisan migrate:rollback php artisan migrate
Team-Scoped Not Working
Problem: Permissions not scoped to tenant
Solution:
// Make sure you're setting the team context setPermissionsTeamId($tenantId); // Or ensure middleware is applied Route::middleware(['set.tenant'])->group(function () { // Your routes });
Permission Not Found
Problem: Spatie\Permission\Exceptions\PermissionDoesNotExist
Solution:
# Sync permissions from config php artisan roles:sync # Or create manually via API POST /admin/acl/permissions
๐ Additional Resources
Documentation Files
- INSTALLATION_GUIDE.md - Detailed installation steps
- SUCCESS_SUMMARY.md - Package features and achievements
- FINAL_TEST_RESULTS.md - Complete test results
- GIT_RELEASE_SUMMARY.md - Release information
API Examples
See the tests/Feature/ directory for comprehensive API usage examples.
Spatie Permission Docs
This package extends Spatie Laravel Permission. For advanced usage, refer to their documentation.
๐ค Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
./vendor/bin/pest) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
๐ License
This package is open-source software licensed under the MIT license.
๐ Credits
- Built on Spatie Laravel Permission
- Developed by Enad Abuzaid
๐ง Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: enad.abuzaid15@gmail.com
โญ Star This Repository
If you find this package helpful, please consider giving it a star on GitHub!
Version: 1.1.0
Status: Production Ready โ
Tests: 32/32 Passing (100%) ๐