The package dispatches events when permissions and roles are attached or detached, allowing you to react to permission changes in your application.
Configuration
Events are disabled by default . Enable them in your configuration:
'events_enabled' => true ,
Enabling events may impact performance, especially when bulk assigning permissions. Only enable if you need to react to permission changes.
Available Events
The package provides four event classes:
Fired when one or more permissions are attached to a model. namespace Spatie\Permission\Events ;
use Illuminate\Database\Eloquent\ Model ;
use Illuminate\Support\ Collection ;
use Spatie\Permission\Contracts\ Permission ;
class PermissionAttachedEvent
{
use Dispatchable ;
use InteractsWithSockets ;
use SerializesModels ;
/**
* @param array | int []| string[] | Permission | Permission []| Collection $permissionsOrIds
*/
public function __construct (
public Model $model ,
public mixed $permissionsOrIds
) {}
}
Fired when one or more permissions are removed from a model. namespace Spatie\Permission\Events ;
use Illuminate\Database\Eloquent\ Model ;
use Illuminate\Support\ Collection ;
use Spatie\Permission\Contracts\ Permission ;
class PermissionDetachedEvent
{
use Dispatchable ;
use InteractsWithSockets ;
use SerializesModels ;
/**
* @param array | int []| string[] | Permission | Permission []| Collection $permissionsOrIds
*/
public function __construct (
public Model $model ,
public mixed $permissionsOrIds
) {}
}
Fired when one or more roles are attached to a model. namespace Spatie\Permission\Events ;
use Illuminate\Database\Eloquent\ Model ;
use Illuminate\Support\ Collection ;
use Spatie\Permission\Contracts\ Role ;
class RoleAttachedEvent
{
use Dispatchable ;
use InteractsWithSockets ;
use SerializesModels ;
/**
* @param array | int []| string[] | Role | Role []| Collection $rolesOrIds
*/
public function __construct (
public Model $model ,
public mixed $rolesOrIds
) {}
}
Fired when one or more roles are removed from a model. namespace Spatie\Permission\Events ;
use Illuminate\Database\Eloquent\ Model ;
use Illuminate\Support\ Collection ;
use Spatie\Permission\Contracts\ Role ;
class RoleDetachedEvent
{
use Dispatchable ;
use InteractsWithSockets ;
use SerializesModels ;
/**
* @param array | int []| string[] | Role | Role []| Collection $rolesOrIds
*/
public function __construct (
public Model $model ,
public mixed $rolesOrIds
) {}
}
Event Properties
All events have two public properties:
model - The model that received or lost permissions/roles (e.g., User, Role)
permissionsOrIds / rolesOrIds - The permissions/roles that were attached or detached
The second property type varies:
For attach events: usually an array of IDs (integers or UUIDs)
For detach events: can be Permission/Role models or collections
Always inspect the type in your listener before using.
Creating Listeners
Generate Listener
php artisan make:listener LogPermissionChanges
Listener Examples
app/Listeners/LogPermissionChanges.php
namespace App\Listeners ;
use Illuminate\Support\Facades\ Log ;
use Spatie\Permission\Events\ PermissionAttachedEvent ;
use Spatie\Permission\Events\ PermissionDetachedEvent ;
use Spatie\Permission\Models\ Permission ;
class LogPermissionChanges
{
public function handleAttached ( PermissionAttachedEvent $event )
{
$model = $event -> model ;
$permissions = $this -> resolvePermissions ( $event -> permissionsOrIds );
Log :: info ( 'Permissions attached' , [
'model' => get_class ( $model ),
'model_id' => $model -> id ,
'permissions' => $permissions ,
]);
}
public function handleDetached ( PermissionDetachedEvent $event )
{
$model = $event -> model ;
$permissions = $this -> resolvePermissions ( $event -> permissionsOrIds );
Log :: info ( 'Permissions detached' , [
'model' => get_class ( $model ),
'model_id' => $model -> id ,
'permissions' => $permissions ,
]);
}
private function resolvePermissions ( $permissionsOrIds ) : array
{
// Handle different types
if ( is_array ( $permissionsOrIds )) {
return Permission :: whereIn ( 'id' , $permissionsOrIds )
-> pluck ( 'name' )
-> toArray ();
}
if ( $permissionsOrIds instanceof Permission ) {
return [ $permissionsOrIds -> name ];
}
if ( $permissionsOrIds instanceof \Illuminate\Support\ Collection ) {
return $permissionsOrIds -> pluck ( 'name' ) -> toArray ();
}
return [];
}
}
Send Notification on Role Change
app/Listeners/NotifyRoleChanges.php
namespace App\Listeners ;
use App\Models\ User ;
use App\Notifications\ RoleAssignedNotification ;
use Spatie\Permission\Events\ RoleAttachedEvent ;
use Spatie\Permission\Models\ Role ;
class NotifyRoleChanges
{
public function handle ( RoleAttachedEvent $event )
{
// Only notify for User models
if ( ! $event -> model instanceof User ) {
return ;
}
$user = $event -> model ;
$roleIds = is_array ( $event -> rolesOrIds )
? $event -> rolesOrIds
: [ $event -> rolesOrIds ];
$roles = Role :: whereIn ( 'id' , $roleIds ) -> get ();
// Send notification
$user -> notify ( new RoleAssignedNotification ( $roles ));
}
}
Audit Trail for Permissions
app/Listeners/AuditPermissionChanges.php
namespace App\Listeners ;
use App\Models\ AuditLog ;
use Spatie\Permission\Events\ PermissionAttachedEvent ;
use Spatie\Permission\Events\ PermissionDetachedEvent ;
class AuditPermissionChanges
{
public function handleAttached ( PermissionAttachedEvent $event )
{
AuditLog :: create ([
'user_id' => auth () -> id (),
'action' => 'permission_attached' ,
'auditable_type' => get_class ( $event -> model ),
'auditable_id' => $event -> model -> id ,
'metadata' => [
'permissions' => $event -> permissionsOrIds ,
],
'ip_address' => request () -> ip (),
]);
}
public function handleDetached ( PermissionDetachedEvent $event )
{
AuditLog :: create ([
'user_id' => auth () -> id (),
'action' => 'permission_detached' ,
'auditable_type' => get_class ( $event -> model ),
'auditable_id' => $event -> model -> id ,
'metadata' => [
'permissions' => $this -> extractPermissionData ( $event -> permissionsOrIds ),
],
'ip_address' => request () -> ip (),
]);
}
private function extractPermissionData ( $permissionsOrIds ) : array
{
if ( is_array ( $permissionsOrIds )) {
return $permissionsOrIds ;
}
if ( $permissionsOrIds instanceof \Illuminate\Support\ Collection ) {
return $permissionsOrIds -> pluck ( 'id' ) -> toArray ();
}
return [];
}
}
Registering Listeners
Register your listeners in EventServiceProvider:
app/Providers/EventServiceProvider.php
namespace App\Providers ;
use App\Listeners\ LogPermissionChanges ;
use App\Listeners\ NotifyRoleChanges ;
use App\Listeners\ AuditPermissionChanges ;
use Illuminate\Foundation\Support\Providers\ EventServiceProvider as ServiceProvider ;
use Spatie\Permission\Events\ PermissionAttachedEvent ;
use Spatie\Permission\Events\ PermissionDetachedEvent ;
use Spatie\Permission\Events\ RoleAttachedEvent ;
use Spatie\Permission\Events\ RoleDetachedEvent ;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
PermissionAttachedEvent :: class => [
[ LogPermissionChanges :: class , 'handleAttached' ],
[ AuditPermissionChanges :: class , 'handleAttached' ],
],
PermissionDetachedEvent :: class => [
[ LogPermissionChanges :: class , 'handleDetached' ],
[ AuditPermissionChanges :: class , 'handleDetached' ],
],
RoleAttachedEvent :: class => [
NotifyRoleChanges :: class ,
],
RoleDetachedEvent :: class => [
// Add role detach listeners
],
];
}
When Events Fire
Events fire when using these methods:
Permission Events
// Fires PermissionAttachedEvent
$user -> givePermissionTo ( 'edit posts' );
$role -> givePermissionTo ([ 'edit posts' , 'delete posts' ]);
$user -> syncPermissions ([ 'edit posts' ]); // Fires both attach and detach
// Fires PermissionDetachedEvent
$user -> revokePermissionTo ( 'edit posts' );
$role -> revokePermissionTo ( 'delete posts' );
$user -> syncPermissions ([]); // Detaches all
Role Events
// Fires RoleAttachedEvent
$user -> assignRole ( 'editor' );
$user -> assignRole ([ 'editor' , 'author' ]);
$user -> syncRoles ([ 'editor' ]); // Fires both attach and detach
// Fires RoleDetachedEvent
$user -> removeRole ( 'editor' );
$user -> removeRole ([ 'editor' , 'author' ]);
$user -> syncRoles ([]); // Detaches all
syncPermissions() and syncRoles() may fire both attach AND detach events in a single call.
Event Data Types
Understanding the Payload
The event payload type varies based on the operation:
use Spatie\Permission\Events\ PermissionAttachedEvent ;
public function handle ( PermissionAttachedEvent $event )
{
$payload = $event -> permissionsOrIds ;
// Could be:
// - array of IDs: [1, 2, 3]
// - array of UUIDs: ['uuid-1', 'uuid-2']
// - Permission model
// - Collection of Permission models
// Safe approach: type check
if ( is_array ( $payload )) {
// Array of IDs
$permissions = Permission :: whereIn ( 'id' , $payload ) -> get ();
} elseif ( $payload instanceof \Illuminate\Support\ Collection ) {
// Already a collection
$permissions = $payload ;
} elseif ( $payload instanceof Permission ) {
// Single permission
$permissions = collect ([ $payload ]);
}
}
Practical Use Cases
Clear User Cache on Permission Change
class ClearUserCacheListener
{
public function handle ( $event )
{
$model = $event -> model ;
// Clear user-specific cache
if ( $model instanceof User ) {
Cache :: forget ( "user.{ $model -> id }.permissions" );
Cache :: forget ( "user.{ $model -> id }.roles" );
}
}
}
Sync Permissions to External Service
class SyncPermissionsToAPI
{
public function handle ( PermissionAttachedEvent $event )
{
if ( $event -> model instanceof User ) {
Http :: post ( 'https://api.example.com/sync-permissions' , [
'user_id' => $event -> model -> id ,
'permissions' => $event -> permissionsOrIds ,
]);
}
}
}
Track Permission History
class TrackPermissionHistory
{
public function handleAttached ( PermissionAttachedEvent $event )
{
$this -> record ( 'attached' , $event );
}
public function handleDetached ( PermissionDetachedEvent $event )
{
$this -> record ( 'detached' , $event );
}
private function record ( string $action , $event )
{
DB :: table ( 'permission_history' ) -> insert ([
'model_type' => get_class ( $event -> model ),
'model_id' => $event -> model -> id ,
'action' => $action ,
'permissions' => json_encode ( $event -> permissionsOrIds ),
'changed_by' => auth () -> id (),
'created_at' => now (),
]);
}
}
Each event fires synchronously and can slow down bulk operations: // This fires 100 PermissionAttachedEvent events!
foreach ( $permissions as $permission ) {
$user -> givePermissionTo ( $permission );
}
Consider using queued listeners for expensive operations.
Queued Listeners
app/Listeners/QueuedPermissionLogger.php
namespace App\Listeners ;
use Illuminate\Contracts\Queue\ ShouldQueue ;
use Illuminate\Queue\ InteractsWithQueue ;
use Spatie\Permission\Events\ PermissionAttachedEvent ;
class QueuedPermissionLogger implements ShouldQueue
{
use InteractsWithQueue ;
public function handle ( PermissionAttachedEvent $event )
{
// This runs in a queue job
Log :: info ( 'Permission attached (queued)' , [
'model_id' => $event -> model -> id ,
'permissions' => $event -> permissionsOrIds ,
]);
}
}
Testing Events
use Illuminate\Support\Facades\ Event ;
use Spatie\Permission\Events\ RoleAttachedEvent ;
public function test_role_attached_event_fires ()
{
Event :: fake ([ RoleAttachedEvent :: class ]);
$user = User :: factory () -> create ();
$user -> assignRole ( 'editor' );
Event :: assertDispatched ( RoleAttachedEvent :: class , function ( $event ) use ( $user ) {
return $event -> model -> id === $user -> id ;
});
}
Disabling Events Temporarily
// Disable events for bulk operations
config ([ 'permission.events_enabled' => false ]);
foreach ( $users as $user ) {
$user -> assignRole ( 'editor' );
}
// Re-enable
config ([ 'permission.events_enabled' => true ]);
Best Practices
Only enable events if you need to react to permission changes
Use queued listeners for expensive operations (API calls, external syncs)
Type-check event payloads before using them
Consider disabling events during seeders and bulk operations
Cache expensive lookups within event listeners