Use php enums to manage permissions in Laravel.

Managing permissions for your application can seem daunting at first. This is due in no small part to how complicated IAM systems can become for large applications. Thankfully, once we leave infrastructure and head into the application, managing users and permissions becomes comparatively simple. And if you’re using Laravel, managing permissions can actually be fun! I’ve said this before, and I’ll say it a thousand times. Laravel makes everything fun.

What are enums? We will get to that later. What are permissions? I’m glad you asked! At their core, permissions are just two things:

  1. A representation of an action, e.g., “edit projects”

  2. An attachment of that representation to a user, e.g., “Vanessa can edit projects”

Anyone familiar with databases can see right away that all we need is a table for permissions, a table for users, and a join table for both. BOOM! We are managing permissions. This is an oversimplification of how you might want to manage permissions, but at their core, this is all permissions need to be.

We only need a table for permissions, a table for users, and a join table for both. BOOM! We are managing permissions.

Ok, enums. Where do they factor in here? Enums come in at the code layer, the “business” layer, to help enforce a certain set of values. To me, there are two major benefits they have for permissions:

  1. Seeding permissions becomes a breeze

  2. Enforcing naming conventions for permissions prevents bugs

Seeding Permissions with Enums

Think about all your models. Think about the stuff in your app. Maybe it's projects, deliverables, and notes? Maybe it's shipments, agents, and invoices? Maybe it's furries and fur suits? I don’t know, you do you. The point is, you probably have a lot of important stuff. And you are going to need multiple permissions for each stuff because remember:

Its all CRUDCreate furry, read furry, update furry, delete furry. Do you want to write all that by hand for each of your stuffs? Of course not; you’re a developer! You want to automate adding <strong> tags around text. Some of you learn VIM, so you never have to touch a mouse. Efficiency is the name of the game. Not to mention, that’s a whole lot of magic strings to have to remember and potentially (inevitably) screw up. Enter enums.

namespace App\Enums;

enum CrudPermissionEnum : string

{
   case CREATE = 'create';
   case READ = 'read';
   case UPDATE = 'update';
   case DELETE = 'delete';
}

This is an enum. More specifically, it is a “backed” enum. This means that the enum represents an underlying scalar value, in this case, a string. When using enums backed by a string, you can enforce values for strings in the business layer before they get to your database. I know some of you are screaming right now, “You can implement enums in the database!” Sure, you can, but it isn’t the best. This very old but still relevant article outlines many great reasons why it's not ideal. And not every database supports enums as a type. Hence, we are using enums in PHP BEFORE they get to the database. Let your business layer handle the business logic, and let your database just say, “OK, boss.”

So, we have our enum mapping CRUD operations to a string value. It is not too useful, but it is the first building block to our amazing permission seeding solution. Step two is mapping models to enums:

namespace App\Enums;

enum ModelEnum : string

{
   case PROJECT = 'projects';
   case DELIVERABLE = 'deliverables';
   case COMMENT = 'comment';
   ...
}

Using an enum, map all the models you need permissions for to a permission-friendly name. Common convention, just use the plural form of your model name.

Now that we have our models and permissions in enums, we can easily write a function to generate common permissions for every model. In my experience, this is best done in a seeder:

namespace Database\Seeders;

use YourPermissionModel;

class PermissionSeeder extends Seeder
{

 	public function run(): void
 	{
   		$crudPermissions = CrudPermissionEnum::cases();
		$models = ModelEnum::cases();
		...
	}
}

Pause here. Cases? This handy function gives us all the cases of an enum in an array. So, with some simple nested loops, we can seed all our permissions:

foreach($models as $m) {
	foreach ($crudPermissions as $p) {
		YourPermissionModel::create([name => "{$p->value} {$m->value}"]);
	}
}

With a few lines of code, we’ve created crud permissions for every model we want. But that’s not even the best we can do. How about a slight refactor? Instead of nested foreach loops:

$crudPermissions = collect(CrudPermissionEnum::cases());
$models = collect(ModelEnum::cases());

// Use reduce instead of map so we avoid having to flatten a nested array
$permissions = $crudPermissions->reduce(function($carry, $p){
	$models->each(function($m) use ($p){
		$carry->push(['name' => "{p->value} {$m->value}"]);
	});
	return $carry;
}, collect());

YourPermissionModel::upsert($permissions);

Wait, did we add more lines of code? Yes, you sweet summer child! But we are now using the upsert instead of the create method. This has the benefit of making one database call and creating new permissions ONLY if they don’t already exist in the database. So if we add a model or permission and run our seeder again, this bad boy isn’t gonna put up a fuss.

Enforcing naming conventions with enums

Alright, we have our permissions seeded. We have a route to one of our models that requires permissions. Let the power of enums show itself once more:

Route::get('/fur-suits', function(Request $request){
	$user = auth()->user();
	try {
   		$can = $user->permissions()
			->where('name', '=', "{CrudPermissionEnum::READ->value} {ModelEnum::FURSUIT->value}")
			->firstOrFail();

   		return "You can view this!";
	} 
	catch(\Exception $e) {
		return $response("You can't do that", 401);
	 }
});

Admittedly, typing out the names of those enums is cumbersome. The good news is that your text editor will auto-load most of the names for you. The better news is that you won’t forget your permissions' names anytime soon. Think about how much more cumbersome it is to reference a separate list of permission names, even if they are in a config file.

Typically, you’d want to rely on middleware for routing authorization and a policy for models. You can reference the permissions for a user within the policy.

// App\Policies

class FurSuitPolicy
{
	public function read(User $user)
	{
		$name = "{CrudPermissionEnum::READ->value} {ModelEnum::FURSUIT->value}";
		return $user->permissions()->where('name', $name)->exists();
	}

}

// Routes
Route::get('/fur-suits', function(Request $request){
	return "You can do that!"
})->can('read', FurSuit::class);

Perhaps within this fur suit route, you want the user to be able to edit the fur suit. After all, who wouldn’t want to do that? But they need the permission first. Let’s pretend a magic method exists to check if a user has permission by string name (if you are using a package like Laravel Permission, this is built in. And, they recommend using enums!)

// A blade file

@if (auth()->user()->hasPermission("{CrudPermissionEnum::UPDATE->value} {ModelEnum::FURSUIT->value}"))
	You can edit this
@endif

We’ve switched from policy file to blade template without losing track of the permission names and how to call them because we are using enums. And because we started with enums when seeding the database, they will be consistent throughout the application. No bugs when we misspell a model, forget we used plural for the naming convention, or forget that instead of “edit fursuit,” it is “update fursuits.” No checking for the WRONG permission when trying to safeguard a route. Nope, all that is not an issue when we stick with enums as our preferred way to create and access permissions.

Hopefully, you can see the value of using enums to seed and manage permissions. I will be writing more about managing permissions yourself in future articles. Stay tuned!