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_ 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_ |
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#
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:
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):
// 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:
$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') |
\ (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:
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:
- Remove the old package (
composer remove spatie/laravel-permission) and its traits from your models. - Rename the old tables (for example
archived_roles) rather than dropping them straight away. - Drop them after a release cycle with no problems.