whilesmart/eloquent-roles
A lightweight Laravel package for assigning roles and permissions to Eloquent models. Define roles, attach them to users or any model, and check abilities with simple, expressive methods—no heavy dependencies or complex setup required.
Install the package and wire it into a model:
composer require whilesmart/eloquent-roles
php artisan vendor:publish --provider="Whilesmart\EloquentRoles\EloquentRolesServiceProvider"
php artisan migrate
Publishing typically gives you a config file and the base roles / permissions (and likely pivot) migrations. Check the migration output before running it in production — pivot table names and column types are the things most likely to need adjustment for an existing schema.
Add the trait to any authenticatable model that should hold roles:
use Whilesmart\EloquentRoles\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Seed your role catalog before assigning anything. A dedicated seeder that creates the canonical role names once, and is idempotent (firstOrCreate), pays for itself immediately:
foreach (['admin', 'manager', 'member'] as $name) {
Role::firstOrCreate(['name' => $name]);
}
Where to look first: read the package's HasRoles trait source rather than just the README. Method names for assignment and checking (assignRole, hasRole, hasPermissionTo, or a givePermissionTo variant) are the entire day-to-day API, and small packages like this one drift from their docs. Once you've confirmed the method signatures, your first real use case is almost always a gate check in a controller or middleware:
if (! $request->user()->hasRole('admin')) {
abort(403);
}
Seed-driven role catalog. Treat roles as reference data, not user data. Define them in a seeder, keep the list short, and never create roles ad hoc in application code. This keeps hasRole('admin') string comparisons from silently failing when someone typos a role name.
Guard the assignment boundary. Role assignment should live behind an admin-only endpoint or an artisan command, never in a registration flow or mass-assignment path. The package's pivot writes are unguarded — if a $fillable-adjacent assignment path exists, a user could grant themselves a role.
Middleware for route protection, policies for resource-level checks. Use the package for the coarse "can this user reach this area" gate, and keep ownership/state logic in Laravel policies. Mixing the two produces authorization that's hard to audit:
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::resource('users', UserController::class);
});
If the package doesn't ship middleware, wrap the check in a one-line custom middleware rather than repeating hasRole in every controller.
Blade and form request integration. Expose the checks where views and validation live:
@if(auth()->user()->hasRole('manager'))
<a href="{{ route('reports.index') }}">Reports</a>
@endif
And in form requests, authorize() becomes a clean single line once the trait is on the model.
Eager-load role relationships on list screens. An index page rendering a role badge per row will fire N+1 queries unless you eager-load the roles relation. Add it to the query builder or a base repository method.
Cache the permission set per request, not per session. If you're computing hasPermissionTo repeatedly inside a loop, memoize the resolved role/permission collection on the model instance for the request lifecycle. Don't persist it to Redis across requests unless you've solved invalidation.
Guard mismatch is the classic failure. If your roles table has a guard_name column and it defaults to web while you're checking against a sanctum or api guard, hasRole returns false with no error. Confirm the guard the package writes against matches the guard your routes authenticate with.
Cache invalidation on assignment. Any caching layer over roles — whether the package's or your own — must be invalidated the moment assignRole/removeRole runs. Wrap assignments in a small service method that clears the cache so no caller forgets.
String vs. object comparison. Passing a Role model where a string is expected (or vice versa) is the second-most-common mistake. Pick one convention across your codebase and enforce it.
Migration ordering. The package migrations must run after your users table exists and before any migration that references the pivot tables. If you're on an existing app, check the timestamp ordering after publishing.
Super-admin bypass. Add a Gate::before in AuthServiceProvider that grants everything to a designated super-admin role. It's one line and it removes dozens of hasRole('admin') || hasRole('super-admin') conditionals:
Gate::before(fn ($user) => $user->hasRole('super-admin') ? true : null);
Note the : null — returning false here would deny everyone else.
Testing. Use RefreshDatabase and seed roles in the test setup. For feature tests, create a helper like actingAsAdmin() that assigns the role in one call; otherwise every test re-implements the assignment.
Extension points. The Role and Permission models are typically extendable via the config file — if you need tenant scoping or a label column, override the model in config rather than editing vendor files. Check the service provider for a bind/swap hook.
Small-package caution. With a low dependent count, expect thinner edge-case coverage than spatie/laravel-permission. Before committing, write one integration test that covers assignment, checking, and removal end-to-end — that test is your safety net if the package's API shifts between releases.
How can I help you explore Laravel packages today?