Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

laracoft's avatar

Adding roles and permissions

  1. Most authorization packages store their roles and permissions in the database
  2. On day 1, I might have 2 roles, buyer and seller. I think most people will add this using a seeder
  3. On day 30, I need to add another 2 roles, admin and agent

If I modify the seeder on day 30, it will add buyer and seller roles again

How do you guys progressively populate these roles and permissions over time?

0 likes
17 replies
jlrdw's avatar

Roles is a table, you don't seed again.

mrkarma4ya's avatar

I use firstOrCreate to seed new roles and permissions so I don't create duplicates and can re-use the same seeder by just updating the roles and permissions array.

1 like
jlrdw's avatar

Seeding is test data. Inserting (example inserting a new post here) is inserting a database record. WIll @jeffreyway have to reseed the laracast database after every post or reply, no.

You create a roles table definition and migrate. Adding data does not modify the structure whether 5 roles or 200 roles.

MohamedTammam's avatar
Level 51

Here's an example of my roles and permission seeders using Spatie permissions package. I use firstOrCreate for not duplicating roles and permissions.

And I store added roles and permissions in arrays to delete them, that way the seeder will also delete unwanted roles and permission.

public function run()
{
        // Reset cached roles and permissions
        app()[PermissionRegistrar::class]->forgetCachedPermissions();

        // Roles and permissions
        $roles = [
            [
                'name' => 'Role Name',
                'permissions' => [
					'Permission Name',
				],
            ],
            [
                'name' => 'Role without permission's, 
                'permissions' => [],
            ],
        ];

        $addedRoles = [];
        $addedPermissions = [];

        foreach ($roles as $roleMap) {
            $role = Role::firstOrCreate(['name' => $roleMap['name']]);

            $addedRoles[] = $roleMap['name'];

            foreach ($roleMap['permissions'] as $permissionName) {
                $permission = Permission::firstOrCreate(['name' => $permissionName]);
                $role->givePermissionTo($permission);

                $addedPermissions[] = $permissionName;
            }
        }

        Role::whereNotIn('name', $addedRoles)->delete();
        Permission::whereNotIn('name', $addedPermissions)->delete();
}

I used seeder in that project because it was making since. However you can also create a command that will add necessary roles if seeders isn't a good for your case.

2 likes
laracoft's avatar

@MohamedTammam

Thanks, there is a small group of data that is used by production code, which in my view should be populated by code. Roles and permissions is an example of such data. They are seeds, but need to behave like migrations.

If I create a new command, new developer will need to become aware of it and remember to run it. So, I'm not for it.

Elliot_putt's avatar

@laracoft If this helps on my apps the end users want new roles all the time. so I have a Laravel job which has its own route and controller function in the settings on the app you can click a button to do firstOrCreate and then this will add our default roles instead of a command and then if they want to add their own role we have a model for roles which the user can check what the role should and shouldn't have access to models wise.

laracoft's avatar

@Elliot_putt

I see. My users are not allowed to create roles :)

After some thought, I think it might be easier to just populate these roles using migrations that do not alter tables at all. This way, they would be tracked (run only once).

And I can create more migrations if I need to add more roles and permissions.

jlrdw's avatar

@laracoft Why would you need more migrations if adding more roles and permissions?

They are tables, so you can just add to (insert new record) for a role or permission.

// before

admin
user
// after

admin
user
bookkeeper   // added role
chemist    //  added role

You only need an initial migration, unless more fields are added, not more records.

laracoft's avatar

@jlrdw ok, here's what I struggled with for a long time and would like to hear ur solution:

  1. admin + user, added on day 1

    • Which PHP file do I place this code in for the roles to reach production?
  2. bookkeeper + chemist + 198 more roles, added on day 30

    • Which PHP file do I place this code in for the roles to reach production?
    • And by adding this on day 30, will I have duplicate admin and user roles in my table?
jlrdw's avatar

@laracoft you don't add it in a file it's a database record you insert like any other data. This is done in a production database.

It's a good idea to make a chart (with pencil and paper) of all of the roles and permissions prior to pushing to production.

Always try to have everything completed before pushing to production.

laracoft's avatar

@jlrdw

so you are saying that I should add 200 roles by hand to a production database?

jlrdw's avatar

@laracoft yes, no different than adding a user, a sales invoice, a post, or anything else, it all goes into a production database.

I personally like RBAC better than permissions.

Don't push to production if you are not ready.

laracoft's avatar

@jlrdw ok, I see roles differently from invoice/post.

Let's say an accounting system,

  1. We will have code like User::hasRole("bookkeeper")
  2. The accounting system doesn't make sense if bookkeeper does not exist after "migrate:fresh --seed"
  3. And even cause exceptions in Nova if certain permissions are not created in the case of spatie's package
  4. When exceptions are thrown, the developer will be left wondering what happened?
  5. And it can be a challenge if I have to add 200 roles by hand to several projects
  6. Compared to a user/post/invoice, these tables can be empty, but the system still makes sense

Anyway, thank you for sharing, I understand how you are handling this case.

PovilasKorop's avatar

@laracoft I'm a bit late to answer here but I would actually use a migration for this.

Yes, you've read it correctly: migration for the data, not for the schema structure.

The migration file has an up() method where you can do whatever you want, not just Schema::create().

And the migration php artisan migrate is a typical command to run for everyone when pulling the new changes, including deployment to live server and automatic CI/CD and testing process with tools like GitHub Actions.

So, it's the most convenient and logical place to ensure that all server environments would have that data.

php artisan make:migration seed_roles

database/migrations/xxxxxxx_seed_roles.php:

use App\Models\Role;
 
return new class extends Migration
{
    public function up()
    {
        Role::create(['name' => 'Publisher']);
        Role::create(['name' => 'Viewer']);
        Role::create(['name' => 'Commenter']);
    }
};

Notice: if you want to make sure that this Role record doesn't already exist in the database, you may run this with firstOrCreate():

Role::firstOrCreate(['name' => 'Publisher']);
1 like
laracoft's avatar

@PovilasKorop

Yes, I came to the same conclusion yesterday after reading @mohamedtammam and @elliot_putt answers. For a while, it felt like overengineering for a simple problem. I'm re-assured to hear you would also take the migration approach.

I have learned alot from all your videos and thank you for that :)

1 like

Please or to participate in this conversation.