Guides

Migrating from Another Role System

On this page

This guide moves an existing app, with its own roles table or Spatie Laravel Permission, onto SystemRoles without locking anyone out.

1. Plan#

Answer these first:

Question Why it matters
Which role is the super admin, and what is its id? Set SYSTEMROLES_SUPER_ADMIN_ROLE_ID to match, so you don't have to renumber
Can a user have more than one role today? SystemRoles allows one. Decide which role wins for users with several
Where are permissions checked today? Middleware, @can, hasRole(), hasPermissionTo(), custom code: each needs replacing
Which tables point at the old roles table? Their foreign keys must move to system_roles

Take a full database backup before you start.

2. Install without seeding#

Follow Installation steps 1–4, but skip the seeder: you will copy your own roles, keeping their ids.

3. Copy roles, keeping ids#

PHP
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

DB::transaction(function () {
    foreach (DB::table('roles')->orderBy('id')->get() as $i => $old) {
        DB::table('system_roles')->insertOrIgnore([
            'role_id'         => $old->id,
            'role_name'       => $old->name,
            'role_guard_name' => Str::snake(strtolower($old->name)),
            'role_sequence'   => $i + 1,
            'created_at'      => now(),
            'updated_at'      => now(),
        ]);
    }
});

Put this in a migration of your own so it runs the same way in every environment. Then set:

.env
SYSTEMROLES_SUPER_ADMIN_ROLE_ID=<your super admin id>

4. Copy user roles#

One row per user. For users with several roles, pick one, for example the lowest id (often the most powerful):

PHP
// From Spatie's model_has_roles
$rows = DB::table('model_has_roles')
    ->where('model_type', \App\Models\User::class)
    ->select('model_id as user_id', DB::raw('MIN(role_id) as role_id'))
    ->groupBy('model_id')
    ->get();

foreach ($rows as $row) {
    DB::table('user_roles')->updateOrInsert(
        ['user_id' => $row->user_id],
        ['user_role' => $row->role_id, 'role_type' => 'migrated', 'created_at' => now(), 'updated_at' => now()]
    );
}

Export the list of users who had several roles, so someone can review them.

5. Copy permissions#

From Spatie (permissions + role_has_permissions), build the full matrix:

PHP
$names  = DB::table('permissions')->pluck('name');
$grants = DB::table('role_has_permissions')
    ->join('permissions', 'permissions.id', '=', 'role_has_permissions.permission_id')
    ->get(['role_id', 'permissions.name'])
    ->groupBy('role_id')
    ->map(fn ($g) => $g->pluck('name')->all());

$seq = 0;
foreach (DB::table('system_roles')->pluck('role_id') as $roleId) {
    foreach ($names as $name) {
        DB::table('system_roles_in')->updateOrInsert(
            ['role_id' => $roleId, 'in_name' => $name],
            [
                'in_guard_name' => strtolower($name),
                'in_role'       => in_array($name, $grants[$roleId] ?? [], true) ? '1' : '0',
                'in_sequence'   => ++$seq,
                'created_at'    => now(),
                'updated_at'    => now(),
            ]
        );
    }
}

6. Replace the checks#

Before (Spatie) After (SystemRoles)
$user->hasRole('admin') \BabeRuka\SystemRoles\Models\SystemRoles::find(SystemRolesService::roleIdForUser($user->id))?->role_guard_name === 'admin' (wrap it in a helper)
$user->hasPermissionTo('x') / $user->can('x') SystemRolesService::roleHas($roleId, 'x'), or keep $user->can('x') with the Gate hook
role:admin middleware role:admin from the middleware recipes
permission:x middleware permission:x from the recipes
@role('admin') @role('admin') Blade directive
$user->assignRole('editor') The AssignRole service

Using the same middleware aliases and Blade directive names keeps your routes and views unchanged.

7. Move foreign keys#

Tables that pointed at roles.id should point at system_roles.role_id. Because you kept the ids, only the constraint changes, not the data:

PHP
Schema::table('some_table', function (Blueprint $table) {
    $table->dropForeign(['role_id']);
    $table->foreign('role_id')->references('role_id')->on('system_roles');
});

8. Roll out in audit mode#

Turn the new checks on in log-only mode first (see audit mode). Compare the "would deny" log with what people should be able to do, fix the matrix, then enforce.

9. Retire the old tables#

When everything runs on SystemRoles:

  1. Remove the old package (composer remove spatie/laravel-permission) and its traits from your models.
  2. Rename the old tables (for example archived_roles) rather than dropping them straight away.
  3. Drop them after a release cycle with no problems.