Skip to content

Commit d1eab37

Browse files
sven-wegnersven-arztkonsultationdrbyte
authored
4 events for adding and removing roles or permissions (#2742)
* Added Events `PermissionAttached`, `PermissionDetached`, `RoleAttached` and `RoleDetached` --------- Co-authored-by: Sven Wegner <[email protected]> Co-authored-by: Chris Brown <[email protected]>
1 parent 9070fcd commit d1eab37

9 files changed

+253
-2
lines changed

config/permission.php

+11
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@
110110
*/
111111
'register_octane_reset_listener' => false,
112112

113+
/*
114+
* Events will fire when a role or permission is assigned/unassigned:
115+
* \Spatie\Permission\Events\RoleAttached
116+
* \Spatie\Permission\Events\RoleDetached
117+
* \Spatie\Permission\Events\PermissionAttached
118+
* \Spatie\Permission\Events\PermissionDetached
119+
*
120+
* To enable, set to true, and then create listeners to watch these events.
121+
*/
122+
'events_enabled' => false,
123+
113124
/*
114125
* Teams Feature.
115126
* When set to true the package implements teams using the 'team_foreign_key'.

src/Events/PermissionAttached.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spatie\Permission\Events;
6+
7+
use Illuminate\Broadcasting\InteractsWithSockets;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Collection;
12+
use Spatie\Permission\Contracts\Permission;
13+
14+
class PermissionAttached
15+
{
16+
use Dispatchable;
17+
use InteractsWithSockets;
18+
use SerializesModels;
19+
20+
/**
21+
* Internally the HasPermissions trait passes an array of permission ids (eg: int's or uuid's)
22+
* Theoretically one could register the event to other places and pass an Eloquent record.
23+
* So a Listener should inspect the type of $permissionsOrIds received before using.
24+
*
25+
* @param array|int[]|string[]|Permission|Permission[]|Collection $permissionsOrIds
26+
*/
27+
public function __construct(public Model $model, public mixed $permissionsOrIds) {}
28+
}

src/Events/PermissionDetached.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spatie\Permission\Events;
6+
7+
use Illuminate\Broadcasting\InteractsWithSockets;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Collection;
12+
use Spatie\Permission\Contracts\Permission;
13+
14+
class PermissionDetached
15+
{
16+
use Dispatchable;
17+
use InteractsWithSockets;
18+
use SerializesModels;
19+
20+
/**
21+
* Internally the HasPermissions trait passes $permissionsOrIds as an Eloquent record.
22+
* Theoretically one could register the event to other places and pass an array etc.
23+
* So a Listener should inspect the type of $permissionsOrIds received before using.
24+
*
25+
* @param array|int[]|string[]|Permission|Permission[]|Collection $permissionsOrIds
26+
*/
27+
public function __construct(public Model $model, public mixed $permissionsOrIds) {}
28+
}

src/Events/RoleAttached.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spatie\Permission\Events;
6+
7+
use Illuminate\Broadcasting\InteractsWithSockets;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Collection;
12+
use Spatie\Permission\Contracts\Role;
13+
14+
class RoleAttached
15+
{
16+
use Dispatchable;
17+
use InteractsWithSockets;
18+
use SerializesModels;
19+
20+
/**
21+
* Internally the HasRoles trait passes an array of role ids (eg: int's or uuid's)
22+
* Theoretically one could register the event to other places passing other types
23+
* So a Listener should inspect the type of $rolesOrIds received before using.
24+
*
25+
* @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds
26+
*/
27+
public function __construct(public Model $model, public mixed $rolesOrIds) {}
28+
}

src/Events/RoleDetached.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spatie\Permission\Events;
6+
7+
use Illuminate\Broadcasting\InteractsWithSockets;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Foundation\Bus\Dispatchable;
10+
use Illuminate\Queue\SerializesModels;
11+
use Illuminate\Support\Collection;
12+
use Spatie\Permission\Contracts\Role;
13+
14+
class RoleDetached
15+
{
16+
use Dispatchable;
17+
use InteractsWithSockets;
18+
use SerializesModels;
19+
20+
/**
21+
* Internally the HasRoles trait passes $rolesOrIds as a single Eloquent record
22+
* Theoretically one could register the event to other places with an array etc
23+
* So a Listener should inspect the type of $rolesOrIds received before using.
24+
*
25+
* @param array|int[]|string[]|Role|Role[]|Collection $rolesOrIds
26+
*/
27+
public function __construct(public Model $model, public mixed $rolesOrIds) {}
28+
}

src/Traits/HasPermissions.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
use Spatie\Permission\Contracts\Permission;
1010
use Spatie\Permission\Contracts\Role;
1111
use Spatie\Permission\Contracts\Wildcard;
12+
use Spatie\Permission\Events\PermissionAttached;
13+
use Spatie\Permission\Events\PermissionDetached;
1214
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
1315
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
1416
use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument;
@@ -423,6 +425,10 @@ function ($object) use ($permissions, $model, $teamPivot, &$saved) {
423425
$this->forgetCachedPermissions();
424426
}
425427

428+
if (config('permission.events_enabled')) {
429+
event(new PermissionAttached($this->getModel(), $permissions));
430+
}
431+
426432
$this->forgetWildcardPermissionIndex();
427433

428434
return $this;
@@ -460,12 +466,18 @@ public function syncPermissions(...$permissions)
460466
*/
461467
public function revokePermissionTo($permission)
462468
{
463-
$this->permissions()->detach($this->getStoredPermission($permission));
469+
$storedPermission = $this->getStoredPermission($permission);
470+
471+
$this->permissions()->detach($storedPermission);
464472

465473
if (is_a($this, Role::class)) {
466474
$this->forgetCachedPermissions();
467475
}
468476

477+
if (config('permission.events_enabled')) {
478+
event(new PermissionDetached($this->getModel(), $storedPermission));
479+
}
480+
469481
$this->forgetWildcardPermissionIndex();
470482

471483
$this->unsetRelation('permissions');

src/Traits/HasRoles.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Illuminate\Support\Collection;
99
use Spatie\Permission\Contracts\Permission;
1010
use Spatie\Permission\Contracts\Role;
11+
use Spatie\Permission\Events\RoleAttached;
12+
use Spatie\Permission\Events\RoleDetached;
1113
use Spatie\Permission\PermissionRegistrar;
1214

1315
trait HasRoles
@@ -176,6 +178,10 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) {
176178
$this->forgetCachedPermissions();
177179
}
178180

181+
if (config('permission.events_enabled')) {
182+
event(new RoleAttached($this->getModel(), $roles));
183+
}
184+
179185
return $this;
180186
}
181187

@@ -186,14 +192,20 @@ function ($object) use ($roles, $model, $teamPivot, &$saved) {
186192
*/
187193
public function removeRole($role)
188194
{
189-
$this->roles()->detach($this->getStoredRole($role));
195+
$storedRole = $this->getStoredRole($role);
196+
197+
$this->roles()->detach($storedRole);
190198

191199
$this->unsetRelation('roles');
192200

193201
if (is_a($this, Permission::class)) {
194202
$this->forgetCachedPermissions();
195203
}
196204

205+
if (config('permission.events_enabled')) {
206+
event(new RoleDetached($this->getModel(), $storedRole));
207+
}
208+
197209
return $this;
198210
}
199211

tests/HasPermissionsTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
namespace Spatie\Permission\Tests;
44

55
use DB;
6+
use Illuminate\Support\Facades\Event;
67
use Illuminate\Database\Eloquent\Model;
78
use PHPUnit\Framework\Attributes\RequiresPhp;
89
use PHPUnit\Framework\Attributes\Test;
910
use Spatie\Permission\Contracts\Permission;
1011
use Spatie\Permission\Contracts\Role;
12+
use Spatie\Permission\Events\PermissionAttached;
13+
use Spatie\Permission\Events\PermissionDetached;
14+
use Spatie\Permission\Events\RoleAttached;
1115
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
1216
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
1317
use Spatie\Permission\Tests\TestModels\SoftDeletingUser;
@@ -824,6 +828,64 @@ public function it_can_reject_permission_based_on_logged_in_user_guard()
824828
]);
825829
}
826830

831+
/** @test */
832+
#[Test]
833+
public function it_fires_an_event_when_a_permission_is_added()
834+
{
835+
Event::fake();
836+
app('config')->set('permission.events_enabled', true);
837+
838+
$this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
839+
840+
$ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])
841+
->pluck($this->testUserPermission->getKeyName())
842+
->toArray();
843+
844+
Event::assertDispatched(PermissionAttached::class, function ($event) use ($ids) {
845+
return $event->model instanceof User
846+
&& $event->model->hasPermissionTo('edit-news')
847+
&& $event->model->hasPermissionTo('edit-articles')
848+
&& $ids === $event->permissionsOrIds;
849+
});
850+
}
851+
852+
/** @test */
853+
#[Test]
854+
public function it_does_not_fire_an_event_when_events_are_not_enabled()
855+
{
856+
Event::fake();
857+
app('config')->set('permission.events_enabled', false);
858+
859+
$this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
860+
861+
$ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])
862+
->pluck($this->testUserPermission->getKeyName())
863+
->toArray();
864+
865+
Event::assertNotDispatched(PermissionAttached::class);
866+
}
867+
868+
/** @test */
869+
#[Test]
870+
public function it_fires_an_event_when_a_permission_is_removed()
871+
{
872+
Event::fake();
873+
app('config')->set('permission.events_enabled', true);
874+
875+
$permissions = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get();
876+
877+
$this->testUser->givePermissionTo($permissions);
878+
879+
$this->testUser->revokePermissionTo($permissions);
880+
881+
Event::assertDispatched(PermissionDetached::class, function ($event) use ($permissions) {
882+
return $event->model instanceof User
883+
&& !$event->model->hasPermissionTo('edit-news')
884+
&& !$event->model->hasPermissionTo('edit-articles')
885+
&& $event->permissionsOrIds === $permissions;
886+
});
887+
}
888+
827889
/** @test */
828890
#[Test]
829891
public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted()

tests/HasRolesTest.php

+42
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Support\Facades\DB;
7+
use Illuminate\Support\Facades\Event;
78
use PHPUnit\Framework\Attributes\RequiresPhp;
89
use PHPUnit\Framework\Attributes\Test;
910
use Spatie\Permission\Contracts\Permission;
1011
use Spatie\Permission\Contracts\Role;
12+
use Spatie\Permission\Events\RoleAttached;
13+
use Spatie\Permission\Events\RoleDetached;
1114
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
1215
use Spatie\Permission\Exceptions\RoleDoesNotExist;
1316
use Spatie\Permission\Tests\TestModels\Admin;
@@ -917,6 +920,45 @@ public function it_does_not_detach_roles_when_user_soft_deleting()
917920
$this->assertTrue($user->hasRole('testRole'));
918921
}
919922

923+
/** @test */
924+
#[Test]
925+
public function it_fires_an_event_when_a_role_is_added()
926+
{
927+
Event::fake();
928+
app('config')->set('permission.events_enabled', true);
929+
930+
$this->testUser->assignRole(['testRole', 'testRole2']);
931+
932+
$roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2'])
933+
->pluck($this->testUserRole->getKeyName())
934+
->toArray();
935+
936+
Event::assertDispatched(RoleAttached::class, function ($event) use ($roleIds) {
937+
return $event->model instanceof User
938+
&& $event->model->hasRole('testRole')
939+
&& $event->model->hasRole('testRole2')
940+
&& $event->rolesOrIds === $roleIds;
941+
});
942+
}
943+
944+
/** @test */
945+
#[Test]
946+
public function it_fires_an_event_when_a_role_is_removed()
947+
{
948+
Event::fake();
949+
app('config')->set('permission.events_enabled', true);
950+
951+
$this->testUser->assignRole('testRole');
952+
953+
$this->testUser->removeRole('testRole');
954+
955+
Event::assertDispatched(RoleDetached::class, function ($event) {
956+
return $event->model instanceof User
957+
&& !$event->model->hasRole('testRole')
958+
&& $event->rolesOrIds->name === 'testRole';
959+
});
960+
}
961+
920962
/** @test */
921963
#[Test]
922964
public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted()

0 commit comments

Comments
 (0)