savannabits/entrust
Savannabits/Entrust adds role-based access control to Laravel, letting you assign roles and permissions to users via Eloquent traits, middleware, and Blade directives for simple authorization checks.
v9.0.0 is a compatibility release: it adds Laravel 10 and 11 support (and, by implication, drops the older framework lines that v8.x targeted). If you are on Laravel 10/11 this is the version you want; if you are still on Laravel 8/9, stay on ^8.1 and plan the framework upgrade first.
Minimal setup:
composer require savannabits/entrust:^9.0
php artisan vendor:publish # pick the Entrust provider/tags for config + migrations
php artisan migrate
Publish both the config (config/entrust.php) and the migrations — the four tables (roles, permissions, role_user, permission_role) are not shipped pre-migrated, and the config is where you override table names and the Role/Permission model classes.
Wire the traits onto your models — this is the step people forget:
// app/Models/User.php
use Zizaco\Entrust\Traits\EntrustUserTrait; // verify namespace in the fork's README
class User extends Authenticatable { use EntrustUserTrait; }
// app/Models/Role.php
use Zizaco\Entrust\Traits\EntrustRoleTrait;
class Role extends Model { use EntrustRoleTrait; protected $fillable = ['name','display_name','description']; }
// app/Models/Permission.php
use Zizaco\Entrust\Traits\EntrustPermissionTrait;
class Permission extends Model { use EntrustPermissionTrait; protected $fillable = ['name','display_name','description']; }
First use, in tinker or a seeder:
$admin = Role::create(['name' => 'admin', 'display_name' => 'Administrator']);
$edit = Permission::create(['name' => 'post.edit', 'display_name' => 'Edit posts']);
$admin->attachPermission($edit);
$user->attachRole('admin'); // accepts a Role instance or name
$user->hasRole('admin'); // true
$user->can('post.edit'); // via the package's permission check
Look first at the published config/entrust.php and the migration file — they define the contract everything else depends on (model classes, table names, pivot columns).
Seed the whole matrix, then assign. Roles and permissions are database rows, not code. A dedicated seeder that idempotently firstOrCreates every role and permission and attaches the mapping is the single highest-value thing you can build; ad-hoc attachRole calls scattered through controllers rot quickly.
Route protection via the bundled middleware. The package ships role/permission/ability middleware. In Laravel 10 you alias them in app/Http/Kernel.php; in Laravel 11 there is no Kernel, so register aliases in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Zizaco\Entrust\Middleware\EntrustRole::class,
'permission' => \Zizaco\Entrust\Middleware\EntrustPermission::class,
'ability' => \Zizaco\Entrust\Middleware\EntrustAbility::class,
]);
})
Then:
Route::middleware('role:admin')->group(...);
Route::middleware('permission:post.edit')->group(...);
Route::middleware('ability:admin|owner,post.edit')->group(...); // roles | role, then permission
Blade directives are registered by the service provider and are the cleanest place for UI-level checks:
@role('admin') <a href="/admin">Admin</a> @endrole
@permission('post.edit') <button>Edit</button> @endpermission
@ability('admin|owner', 'post.edit') ... @endability
Fine-grained authorization belongs in policies, not Entrust middleware. Entrust answers "does this user hold this role/permission globally." It cannot answer "does this user own this post." Compose the two: use a Policy/FormRequest for resource-level checks and call $user->hasRole()/$user->can() inside it.
Super-admin bypass. Register a Gate::before in a service provider that returns true for your super-admin role; otherwise you end up attaching every permission to the admin role forever.
Custom fields. Because Role/Permission models are swappable in config, you can add display_name, description, or a level column and point the config at your own subclasses. Do this before writing admin CRUD.
laravel/framework ^10|^11 and PHP 8.1+. Composer will refuse the upgrade on older apps — that's the breaking change, and it's a framework-upgrade blocker, not something you can shim.can() collision with Laravel's Gate. Entrust's permission check and Illuminate\Auth\Access\Authorizable::can() overlap. Do not assume $user->can('post.edit') and Gate::allows('post.edit') are the same code path — Entrust permissions are not automatically registered as Gate abilities. Pick one style per codebase, and if you need both, register Entrust permissions into the Gate explicitly.attachRole() and then check hasRole() on the same instance in the same request, you may get a stale answer. Re-fetch the user ($user->refresh() / re-query) after mutating roles, and reload in tests between assertions.role_user / permission_role rows. Add onDelete('cascade') in your own migration or clean up in a model/observer deleting hook.config/entrust.php (table names, model classes) requires php artisan config:cache (or a clear) to take effect in a cached production deploy — a classic "works locally, wrong table in prod" bug.Kernel.php snippet into an L11 app. If the service provider does not auto-alias the middleware, manual registration in bootstrap/app.php is mandatory or route middleware silently 500s on "target class [role] does not exist."spatie/laravel-permission, Entrust ships no UI, no multi-guard support, and no teams. Build your own CRUD, and validate/sanitize role names heavily — role assignment is a privilege-escalation surface, so guard the controller that calls attachRole with its own authorization check.Entrust::hasRole() / Entrust::can() facade calls for non-$user contexts, and the ability middleware for compound checks. Everything else you need, you write.How can I help you explore Laravel packages today?