Deleting holders
model_roles and model_permissions are polymorphic pivots, so they cannot foreign-key the holder table — deleting a user leaves its grant rows behind. If your app ever reuses primary keys (truncate and reseed, imports, non-autoincrement strategies), a new record that inherits an old id would silently inherit the deleted holder’s roles and permissions. Prevent that in one of two ways.
Detach on delete
Call forgetAllAuthorization() from the model’s deleting (or deleted) hook — the trait shorthand for Permissions::for($model)->forgetAllAuthorization(). It detaches every role and direct permission in one transaction and flushes the cache once the write commits:
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Permissions\Concerns\HasRoles;
class User extends Authenticatable
{
use HasRoles;
protected static function booted(): void
{
static::deleting(fn (self $model) => $model->forgetAllAuthorization());
}
}Prune orphans
Or sweep periodically. Permissions::pruneOrphans() deletes pivot rows whose model_type + model_id no longer resolve to a model — including rows whose morph type no longer maps to any class — and returns how many rows it removed:
use RoundlyConsulting\Permissions\Facades\Permissions;
Permissions::pruneOrphans(); // int — rows deletedThe prune command runs the same thing and reports the count:
php artisan permissions:prune-orphansSchedule it to run on its own:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('permissions:prune-orphans')->daily();Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.