Skip to content

Commit 2a1f631

Browse files
author
alexphila
committed
Add support for configurable role identifier key (e.g., slug)
1 parent 68f9b48 commit 2a1f631

File tree

5 files changed

+109
-11
lines changed

5 files changed

+109
-11
lines changed

config/permission.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626

2727
'role' => Spatie\Permission\Models\Role::class,
2828

29-
],
29+
],
30+
31+
/*
32+
* This option defines which column on the roles table is used to identify a role
33+
* when checking things like $user->hasRole('admin').
34+
* By default, it uses 'name', but you can change this to 'slug' or any other unique column.
35+
*/
36+
'role_identifier' => 'name',
3037

3138
'table_names' => [
3239

database/migrations/create_permission_tables.php.stub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ return new class extends Migration
3838
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
3939
}
4040
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
41+
$table->string('slug')->nullable();
4142
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
4243
$table->timestamps();
4344
if ($teams || config('permission.testing')) {

src/Models/Role.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,28 @@ public static function findById(int|string $id, ?string $guardName = null): Role
126126
return $role;
127127
}
128128

129+
/**
130+
* Find a role by its configured key (e.g. name or slug) and guard name.
131+
*
132+
* @return RoleContract|Role
133+
*
134+
* @throws RoleDoesNotExist
135+
*/
136+
public static function findByKey(string $value, ?string $guardName = null): RoleContract
137+
{
138+
$guardName = $guardName ?? Guard::getDefaultName(static::class);
139+
$key = config('permission.role_identifier', 'name');
140+
141+
$role = static::findByParam([$key => $value, 'guard_name' => $guardName]);
142+
143+
if (! $role) {
144+
throw RoleDoesNotExist::named($value, $guardName);
145+
}
146+
147+
return $role;
148+
}
149+
150+
129151
/**
130152
* Find or create role by its name (and optionally guardName).
131153
*

src/Traits/HasRoles.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ public function syncRoles(...$roles)
238238
*/
239239
public function hasRole($roles, ?string $guard = null): bool
240240
{
241+
$roleKey = $this->getRoleKey();
242+
241243
$this->loadMissing('roles');
242244

243245
if (is_string($roles) && strpos($roles, '|') !== false) {
@@ -249,7 +251,7 @@ public function hasRole($roles, ?string $guard = null): bool
249251

250252
return $this->roles
251253
->when($guard, fn ($q) => $q->where('guard_name', $guard))
252-
->pluck('name')
254+
->pluck($roleKey)
253255
->contains(function ($name) use ($roles) {
254256
/** @var string|\BackedEnum $name */
255257
if ($name instanceof \BackedEnum) {
@@ -270,8 +272,8 @@ public function hasRole($roles, ?string $guard = null): bool
270272

271273
if (is_string($roles)) {
272274
return $guard
273-
? $this->roles->where('guard_name', $guard)->contains('name', $roles)
274-
: $this->roles->contains('name', $roles);
275+
? $this->roles->where('guard_name', $guard)->contains($roleKey, $roles)
276+
: $this->roles->contains($roleKey, $roles);
275277
}
276278

277279
if ($roles instanceof Role) {
@@ -314,6 +316,8 @@ public function hasAnyRole(...$roles): bool
314316
*/
315317
public function hasAllRoles($roles, ?string $guard = null): bool
316318
{
319+
$roleKey = $this->getRoleKey();
320+
317321
$this->loadMissing('roles');
318322

319323
if ($roles instanceof \BackedEnum) {
@@ -332,16 +336,16 @@ public function hasAllRoles($roles, ?string $guard = null): bool
332336
return $this->roles->contains($roles->getKeyName(), $roles->getKey());
333337
}
334338

335-
$roles = collect()->make($roles)->map(function ($role) {
339+
$roles = collect()->make($roles)->map(function ($role) use ($roleKey) {
336340
if ($role instanceof \BackedEnum) {
337341
return $role->value;
338342
}
339343

340-
return $role instanceof Role ? $role->name : $role;
344+
return $role instanceof Role ? $role->{$roleKey} : $role;
341345
});
342346

343347
$roleNames = $guard
344-
? $this->roles->where('guard_name', $guard)->pluck('name')
348+
? $this->roles->where('guard_name', $guard)->pluck($roleKey)
345349
: $this->getRoleNames();
346350

347351
$roleNames = $roleNames->transform(function ($roleName) {
@@ -362,6 +366,8 @@ public function hasAllRoles($roles, ?string $guard = null): bool
362366
*/
363367
public function hasExactRoles($roles, ?string $guard = null): bool
364368
{
369+
$roleKey = $this->getRoleKey();
370+
365371
$this->loadMissing('roles');
366372

367373
if (is_string($roles) && strpos($roles, '|') !== false) {
@@ -373,10 +379,10 @@ public function hasExactRoles($roles, ?string $guard = null): bool
373379
}
374380

375381
if ($roles instanceof Role) {
376-
$roles = [$roles->name];
382+
$roles = [$roles->{$roleKey}];
377383
}
378384

379-
$roles = collect()->make($roles)->map(fn ($role) => $role instanceof Role ? $role->name : $role
385+
$roles = collect()->make($roles)->map(fn ($role) => $role instanceof Role ? $role->{$roleKey} : $role
380386
);
381387

382388
return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard);
@@ -390,11 +396,17 @@ public function getDirectPermissions(): Collection
390396
return $this->permissions;
391397
}
392398

399+
/**
400+
* Return all role identifiers for the model.
401+
*
402+
* Note: This respects the configured 'role_identifier' column (e.g., 'name' or 'slug'),
403+
* even though the method name is 'getRoleNames' for backward compatibility.
404+
*/
393405
public function getRoleNames(): Collection
394406
{
395407
$this->loadMissing('roles');
396408

397-
return $this->roles->pluck('name');
409+
return $this->roles->pluck($this->getRoleKey());
398410
}
399411

400412
protected function getStoredRole($role): Role
@@ -408,7 +420,7 @@ protected function getStoredRole($role): Role
408420
}
409421

410422
if (is_string($role)) {
411-
return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName());
423+
return $this->getRoleClass()::findByKey($role, $this->getDefaultGuardName());
412424
}
413425

414426
return $role;
@@ -435,4 +447,10 @@ protected function convertPipeToArray(string $pipeString)
435447

436448
return explode('|', trim($pipeString, $quoteCharacter));
437449
}
450+
451+
protected function getRoleKey(): string
452+
{
453+
return config('permission.role_identifier', 'name');
454+
}
455+
438456
}

tests/HasRolesTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,56 @@ public function it_can_determine_that_the_user_does_not_have_a_role()
4646
$role = app(Role::class)->findOrCreate('testRoleInWebGuard2', 'web');
4747
$this->assertFalse($this->testUser->hasRole($role));
4848
}
49+
50+
/** @test */
51+
#[Test]
52+
public function test_has_role_with_default_identifier()
53+
{
54+
config(['permission.role_identifier' => 'name']);
55+
56+
$user = User::create(['email' => '[email protected]']);
57+
58+
app(Role::class)->create(['name' => 'Editor', 'slug' => 'editor', 'guard_name' => 'web']);
59+
60+
$user->assignRole('Editor');
61+
62+
$this->assertTrue($user->hasRole('Editor'));
63+
$this->assertFalse($user->hasRole('editor')); // slug shouldn't work
64+
}
65+
66+
/** @test */
67+
#[Test]
68+
public function test_has_role_with_slug_identifier()
69+
{
70+
config(['permission.role_identifier' => 'slug']);
71+
72+
$user = User::create(['email' => '[email protected]']);
73+
74+
app(Role::class)->create(['name' => 'Editor', 'slug' => 'editor', 'guard_name' => 'web']);
75+
76+
$user->assignRole('editor');
77+
78+
$this->assertTrue($user->hasRole('editor'));
79+
$this->assertFalse($user->hasRole('Editor')); // name shouldn't work
80+
}
81+
82+
/** @test */
83+
#[Test]
84+
public function test_has_all_roles_with_slug_identifier()
85+
{
86+
config(['permission.role_identifier' => 'slug']);
87+
88+
$user = User::create(['email' => '[email protected]']);
89+
90+
app(Role::class)->create(['name' => 'Admin', 'slug' => 'admin', 'guard_name' => 'web']);
91+
app(Role::class)->create(['name' => 'Manager', 'slug' => 'manager', 'guard_name' => 'web']);
92+
93+
$user->assignRole('admin', 'manager');
94+
95+
$this->assertTrue($user->hasAllRoles(['admin', 'manager']));
96+
$this->assertTrue($user->hasExactRoles(['admin', 'manager']));
97+
$this->assertFalse($user->hasExactRoles(['admin']));
98+
}
4999

50100
/**
51101
* @test

0 commit comments

Comments
 (0)