Skip to content

Commit

Permalink
Add withoutRole and withoutPermission scopes
Browse files Browse the repository at this point in the history
This allows the inverse of the prior scopes to now allow finding users that do-not-have the specified role or permission

Ref: #1037
  • Loading branch information
drbyte committed Jul 13, 2023
1 parent 01bcb2f commit e0f59c4
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 10 deletions.
10 changes: 6 additions & 4 deletions docs/basic-usage/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,20 @@ $roles = $user->getRoleNames(); // Returns a collection
```

## Scopes
The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions:
The `HasRoles` trait also adds `role` and `withoutRole` scopes to your models to scope the query to certain roles or permissions:

```php
$users = User::role('writer')->get(); // Returns only users with the role 'writer'
$nonEditors = User::withoutRole('editor')->get(); // Returns only users without the role 'editor'
```

The `role` scope can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.
The `role` and `withoutRole` scopes can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.

The same trait also adds a scope to only get users that have a certain permission.
The same trait also adds scopes to only get users that have or don't have a certain permission.

```php
$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly)
$usersWhoCannotEditArticles = User::withoutPermission('edit articles')->get(); // Returns all users without the permission 'edit articles' (inherited or directly)
```

The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object.
Expand All @@ -97,7 +99,7 @@ Since Role and Permission models are extended from Eloquent models, basic Eloque

```php
$all_users_with_all_their_roles = User::with('roles')->get();
$all_users_with_all_direct_permissions = User::with('permissions')->get();
$all_users_with_all_their_direct_permissions = User::with('permissions')->get();
$all_roles_in_database = Role::all()->pluck('name');
$users_without_any_roles = User::doesntHave('roles')->get();
$all_roles_except_a_and_b = Role::whereNotIn('name', ['role A', 'role B'])->get();
Expand Down
31 changes: 31 additions & 0 deletions src/Traits/HasPermissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ public function scopePermission(Builder $query, $permissions): Builder
);
}

/**
* Scope the model query to only those without certain permissions,
* whether indirectly by role or by direct permission.
*
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
*/
public function scopeWithoutPermission(Builder $query, $permissions, $debug = false): Builder
{
$permissions = $this->convertToPermissionModels($permissions);

$permissionClass = $this->getPermissionClass();
$permissionKey = (new $permissionClass())->getKeyName();
$roleClass = is_a($this, Role::class) ? static::class : $this->getRoleClass();
$roleKey = (new $roleClass())->getKeyName();

$rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(
array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), [])
);

return $query->where(fn (Builder $query) => $query
->whereDoesntHave('permissions', fn (Builder $subQuery) => $subQuery
->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey))
)
->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery
->whereDoesntHave('roles', fn (Builder $subQuery) => $subQuery
->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey))
)
)
);
}

/**
* @param string|int|array|Permission|Collection|\BackedEnum $permissions
*
Expand Down
30 changes: 30 additions & 0 deletions src/Traits/HasRoles.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,36 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder
);
}

/**
* Scope the model query to only those without certain roles.
*
* @param string|int|array|Role|Collection $roles
* @param string $guard
*/
public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder
{
if ($roles instanceof Collection) {
$roles = $roles->all();
}

$roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}

$method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName';

return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName());
}, Arr::wrap($roles));

$roleClass = $this->getRoleClass();
$key = (new $roleClass())->getKeyName();

return $query->whereHas('roles', fn (Builder $subQuery) => $subQuery
->whereNotIn(config('permission.table_names.roles').".$key", \array_column($roles, $key))
);
}

/**
* Returns roles ids as array keys
*
Expand Down
61 changes: 55 additions & 6 deletions tests/HasPermissionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,123 +87,160 @@ public function it_can_scope_users_using_enums()
$enum2 = TestModels\TestRolePermissionsEnum::EDITARTICLES;
$permission1 = app(Permission::class)->findOrCreate($enum1->value, 'web');
$permission2 = app(Permission::class)->findOrCreate($enum2->value, 'web');

User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo([$enum1, $enum2]);
$this->testUserRole->givePermissionTo($enum2);
$user2->assignRole('testRole');

$scopedUsers1 = User::permission($enum2)->get();
$scopedUsers2 = User::permission([$enum1])->get();
$scopedUsers3 = User::withoutPermission([$enum1])->get();
$scopedUsers4 = User::withoutPermission([$enum2])->get();

$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(2, $scopedUsers3->count());
$this->assertEquals(1, $scopedUsers4->count());
}

/** @test */
public function it_can_scope_users_using_a_string()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');

$scopedUsers1 = User::permission('edit-articles')->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
$scopedUsers3 = User::withoutPermission('edit-news')->get();

$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(2, $scopedUsers3->count());
}

/** @test */
public function it_can_scope_users_using_a_int()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo([1, 2]);
$this->testUserRole->givePermissionTo(1);
$user2->assignRole('testRole');

$scopedUsers1 = User::permission(1)->get();
$scopedUsers2 = User::permission([2])->get();
$scopedUsers3 = User::withoutPermission([2])->get();

$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(2, $scopedUsers3->count());
}

/** @test */
public function it_can_scope_users_using_an_array()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');
$user3->assignRole('testRole2');

$scopedUsers1 = User::permission(['edit-articles', 'edit-news'])->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
$scopedUsers3 = User::withoutPermission(['edit-news'])->get();

$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(2, $scopedUsers3->count());
}

/** @test */
public function it_can_scope_users_using_a_collection()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');
$user3->assignRole('testRole2');

$scopedUsers1 = User::permission(collect(['edit-articles', 'edit-news']))->get();
$scopedUsers2 = User::permission(collect(['edit-news']))->get();
$scopedUsers3 = User::withoutPermission(collect(['edit-news']))->get();

$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(2, $scopedUsers3->count());
}

/** @test */
public function it_can_scope_users_using_an_object()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo($this->testUserPermission->name);

$scopedUsers1 = User::permission($this->testUserPermission)->get();
$scopedUsers2 = User::permission([$this->testUserPermission])->get();
$scopedUsers3 = User::permission(collect([$this->testUserPermission]))->get();
$scopedUsers4 = User::withoutPermission(collect([$this->testUserPermission]))->get();

$this->assertEquals(1, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
$this->assertEquals(1, $scopedUsers3->count());
$this->assertEquals(0, $scopedUsers4->count());
}

/** @test */
public function it_can_scope_users_without_permissions_only_role()
public function it_can_scope_users_without_direct_permissions_only_role()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$this->testUserRole->givePermissionTo('edit-articles');
$user1->assignRole('testRole');
$user2->assignRole('testRole');
$user3->assignRole('testRole2');

$scopedUsers = User::permission('edit-articles')->get();
$scopedUsers1 = User::permission('edit-articles')->get();
$scopedUsers2 = User::withoutPermission('edit-articles')->get();

$this->assertEquals(2, $scopedUsers->count());
$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
}

/** @test */
public function it_can_scope_users_without_permissions_only_permission()
public function it_can_scope_users_with_only_direct_permission()
{
User::all()->each(fn($item) => $item->delete());
$user1 = User::create(['email' => '[email protected]']);
$user2 = User::create(['email' => '[email protected]']);
$user3 = User::create(['email' => '[email protected]']);
$user1->givePermissionTo(['edit-news']);
$user2->givePermissionTo(['edit-articles', 'edit-news']);

$scopedUsers = User::permission('edit-news')->get();
$scopedUsers1 = User::permission('edit-news')->get();
$scopedUsers2 = User::withoutPermission('edit-news')->get();

$this->assertEquals(2, $scopedUsers->count());
$this->assertEquals(2, $scopedUsers1->count());
$this->assertEquals(1, $scopedUsers2->count());
}

/** @test */
Expand Down Expand Up @@ -252,6 +289,10 @@ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permi
$this->expectException(PermissionDoesNotExist::class);

User::permission('not defined permission')->get();

$this->expectException(PermissionDoesNotExist::class);

User::withoutPermission('not defined permission')->get();
}

/** @test */
Expand All @@ -261,9 +302,17 @@ public function it_throws_an_exception_when_trying_to_scope_a_permission_from_an

User::permission('testAdminPermission')->get();

$this->expectException(PermissionDoesNotExist::class);

User::withoutPermission('testAdminPermission')->get();

$this->expectException(GuardDoesNotMatch::class);

User::permission($this->testAdminPermission)->get();

$this->expectException(GuardDoesNotMatch::class);

User::withoutPermission($this->testAdminPermission)->get();
}

/** @test */
Expand Down
Loading

0 comments on commit e0f59c4

Please sign in to comment.