Supercharge your Role And Permission management with Custom Laravel Helper Commands & Spatie

handle static permissions from config file and effortlessly 🪄sync it with database and vice versa. rock solid 🪨approach resulting in 0 discrepancies .

Laxit
8 min readNov 5, 2023

--

Problem Statement :

  1. Cumbersome Updates: Modifying permissions manually in a Seeder or database table can be time-consuming and error-prone, especially when dealing with a large number of permissions.
  2. Inflexibility in Dynamic Environments: Dynamic development environments often demand frequent changes to permissions, leading to inefficiencies and delays as the manual adjustment process struggles to keep up.
  3. Complex Tracking Processes: In the absence of an automated system, tracking changes and ensuring consistency across different parts of the application can become convoluted and prone to oversight, leading to potential security risks and functional discrepancies.

Solution :

  1. Automated Updates with Custom Artisan Commands: Our custom Artisan commands offer an automated solution for managing permissions, streamlining the update process and minimizing the risk of human error associated with manual updates.
  2. Dynamic Adaptability through Unified Interface: With the unified command-line application, developers can swiftly adapt to dynamic development environments, enabling seamless modifications and updates to permissions as the project evolves.
  3. Efficient Tracking and Synchronization: By synchronizing permissions between the configuration file and the database, our solution ensures efficient tracking and synchronization, eliminating complexities and reducing the likelihood of security vulnerabilities and functional discrepancies.

Usage :

1. Add relevant permissions according to your project in File

// PermissionSeederData.php
return array(
0 => 'product:create',
// Write down you permissions here
);

2. Just run the sync command 🪄🪄🪄

php artisan app:permission:sync

3. That's it !!! Enjoy the easy life, touch some grass

Setting up the commands

1. Create Permission File

you can put this anywhere you wish, I'd suggest to put it adjacent to a seeder file for faster read.

// PermissionSeederData.php
return array(
0 => 'product:create',
1 => 'product:edit',
2 => 'product:delete',
3 => 'product:view',
4 => 'user:create',
5 => 'user:edit',
6 => 'user:delete',
7 => 'user:view',
// Rest of the permissions
);

1.1 Create Permission Seeder.

php artisan make:seeder PermissionSeeder

1.2 Read Permission file in permission seeder for initial seeding.

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;

class PermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$permissions = include 'PermissionSeederData.php';

foreach ($permissions as $permission) {
Permission::create(['name' => $permission]);
}
}
}

2. Create Commands for each operation

2.1 Add Permission Command

php artisan make:command Permission/AddPermissionCommand

<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;

class AddPermissionCommand extends Command
{
protected $signature = 'app:permission:add {name : The name of the permission}';

protected $description = 'Add a new permission';

public function handle()
{
$permissionName = $this->argument('name');

// Check if the permission already exists in the database
if (Permission::where('name', $permissionName)->exists()) {
$this->error("Permission '{$permissionName}' already exists in the database.");
return;
}

// Create the permission in the database
Permission::create(['name' => $permissionName]);
$this->info("Permission '{$permissionName}' added to the database.");

// Add the permission to the file
$filePath = database_path('seeders/PermissionSeederData.php');
$permissions = include $filePath;
if (!in_array($permissionName, $permissions)) {
$permissions[] = $permissionName;
file_put_contents($filePath, '<?php return ' . var_export($permissions, true) . ';');
$this->info("Permission '{$permissionName}' added to the file.");
} else {
$this->error("Permission '{$permissionName}' already exists in the file.");
}
}
}

2.2 List Database Permission Command

php artisan make:command Permission/ListDatabasePermissionCommand
<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;

class ListDatabasePermissionCommand extends Command
{
protected $signature = 'app:permission:list:db';

protected $description = 'List all permissions';

public function handle()
{
$permissions = Permission::latest()->get(['id', 'name', 'guard_name']);

if ($permissions->isEmpty()) {
$this->info('No permissions found.');
} else {
$headers = ['ID', 'Name', 'Guard Name'];
$permissionsData = $permissions->map(function ($permission) {
return $permission->toArray();
});

$this->table($headers, $permissionsData);
}
}
}
$ php artisan app:permission:list:db
+----+-------------------+------------+
| ID | Name | Guard Name |
+----+-------------------+------------+
| 1 | product:create | web |
| 2 | product:edit | web |
| 3 | product:delete | web |
| 4 | product:view | web |
| 5 | user:create | web |
| 6 | user:edit | web |
| 7 | user:delete | web |
| 8 | user:view | web |
| 9 | role:create | web |
| 10 | role:edit | web |
| 11 | role:delete | web |
| 12 | role:view | web |
| 13 | permission:create | web |
| 14 | permission:edit | web |
| 15 | permission:delete | web |
| 16 | permission:view | web |
| 17 | category:create | web |
| 18 | category:edit | web |
| 19 | category:delete | web |
| 20 | category:view | web |
| 21 | order:create | web |
| 22 | order:edit | web |
| 23 | order:delete | web |
| 24 | order:view | web |
| 25 | setting:create | web |
| 26 | setting:edit | web |
| 27 | setting:delete | web |
| 28 | setting:view | web |
+----+-------------------+------------+

2.3 List File permissions Command

php artisan make:command Permission/ListFilePermissionCommand
<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;


class ListFilePermissionCommand extends Command
{
protected $signature = 'app:permission:list:file';

protected $description = 'List all permissions';

public function handle()
{
$filePath = database_path('seeders/PermissionSeederData.php');

if (!file_exists($filePath)) {
$this->error('File not found.');
return;
}

$permissions = include $filePath;

if (empty($permissions)) {
$this->info('No permissions found in the file.');
} else {
$permissionsData = collect($permissions);

$this->info('Permissions:');
foreach ($permissionsData as $permission) {
$this->line($permission);
}
}
}
}
$ php artisan app:permission:list:file
Permissions:
product:create
product:edit
product:delete
product:view
user:create
user:edit
user:delete
user:view

2.4 Compare Permission between Database & File Command

php artisan make:command Permission/ComparePermissionCommand
<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;

class ComparePermissionCommand extends Command
{
protected $signature = 'app:permission:compare';

protected $description = 'Sync permissions from file to database';

public function handle()
{
$filePermissions = include database_path('seeders/PermissionSeederData.php');
$databasePermissions = Permission::pluck('name')->toArray();

$onlyInFile = array_diff($filePermissions, $databasePermissions);
$onlyInDatabase = array_diff($databasePermissions, $filePermissions);

if (empty($onlyInFile) && empty($onlyInDatabase)) {
$this->info('No differences found. Permissions are in sync.');
} else {
if (!empty($onlyInFile)) {
$this->info('Permissions found in file but not in database:');
foreach ($onlyInFile as $permission) {
$this->line($permission);
}
}

if (!empty($onlyInDatabase)) {
$this->info('Permissions found in database but not in file:');
foreach ($onlyInDatabase as $permission) {
$this->line($permission);
}
}
}
}
}
$ php artisan app:permission:compare
Permissions found in file but not in database:
sku:create
sku:edit
sku:delete
sku:view
Permissions found in database but not in file:
category:create
category:delete
category:edit
category:view
order:create
order:delete
order:edit
order:view
permission:create
permission:delete
permission:edit
permission:view
role:create
role:delete
role:edit
role:view
setting:create
setting:delete
setting:edit
setting:view

2.5 Sync Permissions between Database & and File

php artisan make:command Permission/SyncPermissionCommand
<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class SyncPermissionCommand extends Command
{
protected $signature = 'app:permission:sync {--force : Force the synchronization by detaching permissions from roles}';

protected $description = 'Sync permissions from file to database';

public function handle()
{
$filePath = database_path('seeders/PermissionSeederData.php');
$filePermissions = include $filePath;
$databasePermissions = Permission::pluck('name')->toArray();

// Add new permissions from the file to the database
$permissionsToAdd = array_diff($filePermissions, $databasePermissions);
foreach ($permissionsToAdd as $permission) {
Permission::create(['name' => $permission]);
$this->info("Permission '{$permission}' added to the database.");
}

// Remove deleted permissions from the file in the database
$permissionsToRemove = array_diff($databasePermissions, $filePermissions);
foreach ($permissionsToRemove as $permission) {
$permissionModel = Permission::where('name', $permission)->first();
if ($permissionModel) {
$rolesWithPermission = Role::whereHas('permissions', function ($query) use ($permission) {
$query->where('name', $permission);
})->get();
if ($rolesWithPermission->isNotEmpty()) {
$this->error("Permission '{$permission}' is assigned to the following roles:");
foreach ($rolesWithPermission as $role) {
$this->line($role->name);
}
if ($this->option('force')) {
$this->line("Detaching the permission '{$permission}' from the roles.");
foreach ($rolesWithPermission as $role) {
$role->revokePermissionTo($permission);
}
}
}

if ($this->option('force') || $rolesWithPermission->isEmpty()) {
$permissionModel->delete();
$this->info("Permission '{$permission}' removed from the database.");
}
}
}

$this->info('Permissions have been synchronized between the file and the database.');
}
}
# Below command will try to sync permission between database and file.
# This operation may require to delete some permission.
# but if those permissions are already attached to some role.
# the command will only print out a notice and will not remove it.
$ php artisan app:permission:sync
Permission 'sku:create' added to the database.
Permission 'sku:edit' added to the database.
Permission 'sku:delete' added to the database.
Permission 'sku:view' added to the database.
Permission 'category:create' is assigned to the following roles:
super-admin
Permission 'category:delete' is assigned to the following roles:
super-admin
Permission 'category:edit' is assigned to the following roles:
super-admin
# --force flag will detach all the permission from roles
# afterward all those stale permissions will be deleted from system.
# finally, command will synchronize permissoin accros database and file.
$ php artisan app:permission:sync --force
Permission 'category:create' is assigned to the following roles:
super-admin
Detaching the permission 'category:create' from the roles.
Permission 'category:create' removed from the database.
Permissions have been synchronized between the file and the database.

2.6 Remove Permission Command

php artisan make:command Permission/RemovePermissionCommand
<?php

namespace App\Console\Commands\Permission;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;

class RemovePermissionCommand extends Command
{
protected $signature = 'app:permission:remove {name : The name of the permission}';

protected $description = 'Remove a permission';

public function handle()
{
$permissionName = $this->argument('name');

// Check if the permission exists in the database
$permission = Permission::where('name', $permissionName)->first();
if ($permission) {
$permission->delete();
$this->info("Permission '{$permissionName}' removed from the database.");
} else {
$this->error("Permission '{$permissionName}' does not exist in the database.");
return;
}

// Remove the permission from the file
$filePath = database_path('seeders/PermissionSeederData.php');
$permissions = include $filePath;
$key = array_search($permissionName, $permissions);
if ($key !== false) {
unset($permissions[$key]);
file_put_contents($filePath, '<?php return ' . var_export($permissions, true) . ';');
$this->info("Permission '{$permissionName}' removed from the file.");
} else {
$this->error("Permission '{$permissionName}' does not exist in the file.");
}
}
}

3. Kernel Setup

3.1 Load all the commands with custom namespace into the Console Kernel file into getCommands() method.

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{

protected function commands(): void
{
$this->load(__DIR__ . '/Commands');

require base_path('routes/console.php');
}

protected function getCommands(): array
{
return [
\App\Console\Commands\Permission\ListDatabasePermissionCommand::class,
\App\Console\Commands\Permission\ListFilePermissionCommand::class,
\App\Console\Commands\Permission\SyncPermissionCommand::class,
\App\Console\Commands\Permission\ComparePermissionCommand::class,
\App\Console\Commands\Permission\AddPermissionCommand::class,
\App\Console\Commands\Permission\RemovePermissionCommand::class,
];
}
}

3.2 Verify if Commands have been loaded by Laravel kernel or not.

php artisan list 

app
app:permission:add Add a new permission
app:permission:compare Sync permissions from file to database
app:permission:list:db List all permissions
app:permission:list:file List all permissions
app:permission:remove Remove a permission
app:permission:sync Sync permissions from file to database

--

--

Laxit
Laxit

Written by Laxit

Certified AWS DevOps Engineer | Laravel Developer | Wholestack Developer

No responses yet