andrey-helldar/laravel-roles
A lightweight Laravel package for managing user roles, offering a simple way to define roles and assign them to Eloquent models through a minimal API. Low adoption and limited documentation, so verify maintenance and compatibility before using in production.
This is a single-role, column-backed package, not a many-to-many RBAC system. “Getting started” means three things: publish the config/migration, put the trait on your User, and pick a role for existing accounts.
1. Install (mind the era of the package). Last release was July 2020, so it targets Laravel 6/7-era conventions (ServiceProvider auto-discovery, App\Http\Kernel middleware registration). On anything Laravel 9+ expect to verify or hand-patch compatibility rather than assume it works.
composer require andrey-helldar/laravel-roles
2. Publish and migrate. The package ships a config (role definitions) and a migration that adds a role column to your users table.
php artisan vendor:publish --provider="Helldar\Roles\ServiceProvider"
php artisan migrate
3. Attach the trait. Where you put it matters — put it on any Eloquent model you want to gate, not just User.
use Helldar\Roles\Traits\Roles;
class User extends Authenticatable
{
use Roles;
}
4. Define roles once. The config file is the source of truth for valid role names. Treat it like an enum: add admin, manager, editor, viewer, and never hard-code strings elsewhere.
5. First use case — gate an admin area. This is the 15-minute win that justifies the package:
// routes/web.php
Route::middleware(['auth', 'role:admin'])->prefix('admin')->group(function () {
Route::get('/dashboard', [AdminController::class, 'index']);
});
Register the alias in app/Http/Kernel.php:
protected $routeMiddleware = [
// ...
'role' => \Helldar\Roles\Middleware\Role::class,
];
Where to look first when something is off: (a) the published config for exact role names, (b) the trait source for the actual method names (hasRole, isAdmin, setRole — verify against the tag you installed), (c) the middleware source to confirm how it reads the role from the authenticated user, (d) the migration to confirm the column name and type. Because documentation is thin, reading the four files above is faster than searching.
Bootstrap roles in a seeder, not in controllers. New users get a default role at creation; everything else is promoted explicitly.
// database/seeders/UserSeeder.php
User::create([
'name' => 'Admin',
'email' => '[email protected]',
'password' => bcrypt('secret'),
'role' => 'admin',
]);
User::create([
'name' => 'Viewer',
'email' => '[email protected]',
'password' => bcrypt('secret'),
'role' => 'viewer',
]);
Assign on registration via a default in the model or controller. Avoid letting a request payload set the role (mass-assignment hole).
$user = User::create($validated + ['role' => 'viewer']);
// or, if the trait provides a setter:
$user->setRole('viewer');
$user->save();
Check in controllers and services, not just routes. Middleware is coarse; service-level checks handle “can this specific user edit this record.”
public function update(Request $request, Post $post)
{
abort_unless($request->user()->hasRole('admin'), 403);
// ...
}
Use it in Blade for UI affordances only. Hiding a button is UX, not security — always pair with a server-side check.
@if(auth()->user()->hasRole('admin'))
<a href="{{ route('admin.dashboard') }}">Admin</a>
@endif
Group routes by role level, not by individual check. Roles are hierarchical in practice; if admin should also see manager routes, register multiple middleware aliases or check both:
Route::middleware(['auth', 'role:admin,manager'])->group(/* ... */);
Keep role checks out of templates and views that also need data. Resolve the role once in a view composer or policy and pass a boolean ($isAdmin) rather than calling the trait repeatedly.
Testing pattern. Combine actingAs with a seeded role so the middleware path is exercised, not just the trait:
public function test_admin_can_view_dashboard()
{
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin)
->get('/admin/dashboard')
->assertOk();
}
public function test_viewer_is_forbidden()
{
$viewer = User::factory()->create(['role' => 'viewer']);
$this->actingAs($viewer)
->get('/admin/dashboard')
->assertForbidden();
}
Integrate with Gate/Policy rather than replacing it. If you already use policies, define a before hook so admin bypasses rules and the package handles the coarse gate:
Gate::before(function ($user) {
return $user->hasRole('admin') ? true : null;
});
This gives you a migration path to native authorization later without rewriting every controller.
Archived and low-traffic — treat it as vendored code. Last release July 2020, 12 stars, no maintainer activity. That means no security fixes, no Laravel 9/10/11 support out of the box, and potential dependency resolution failures. Pin the version, and be prepared to fork if you need compatibility patches. For production systems with a long horizon, native Gates/Policies or a maintained package is the safer bet; this package is best for small internal tools or legacy apps where you want the smallest possible change.
It is effectively single-role. The design is a role column on the model. If requirements say “a user can be both editor and reviewer,” this package will fight you. Model that as a role plus a separate permissions/flags mechanism, or switch packages rather than hacking a JSON column into the trait.
No Kernel.php on Laravel 11+. The middleware alias registration shown above doesn’t apply. You’d register it in bootstrap/app.php with ->withMiddleware(...), and you’ll need to confirm the middleware class still implements the current handle signature.
Config caching will silently ignore your role edits. If you add a role to the config and it “doesn’t exist,” run php artisan config:clear (or config:cache in deploy). This is the #1 source of “the middleware says forbidden but I set the role” confusion.
Typos in role strings fail open or closed depending on your check. hasRole('admn') returns false, and if you inverted the check somewhere you get a security bug. Centralize names:
final class Role
{
public const ADMIN = 'admin';
public const VIEWER = 'viewer';
}
Then use Role::ADMIN everywhere, including the route middleware string via 'role:'.Role::ADMIN.
Migrations can collide. If your users table already has role, role_id, or a Spatie roles table, the published migration will either fail or create confusion. Inspect the migration before running it and drop columns you don’t need. On a fresh app, this is painless; on an existing app, do it deliberately.
Stale role after change in-session. If an admin changes a user’s role, the authenticated user’s model instance may be cached for the request. Refresh before checking ($user->refresh()) or re-read from the DB in the middleware path if you need immediate effect.
Mass assignment. Add role to $fillable only if you control assignment. The safer pattern is assigning via the trait setter or the seeder, and never accepting role from a request. If it must be fillable for seeders, guard the controller with $request->validate() excluding role.
Debugging checklist. When a check misbehaves: (1) dd($user->getAttribute('role')) to confirm the column value, (2) confirm the trait is on the exact model instance returned by auth()->user() (a custom guard returning a different model class will skip the trait), (3) php artisan route:list to confirm the middleware alias resolved, (4) clear config cache, (5) inspect the middleware source to see whether it aborts or redirects on failure — the status code tells you which layer failed.
Extension points. The trait is plain PHP, so you can override individual methods in your User class (e.g., make hasRole('admin') also return true for a super-admin email), and you can wrap the middleware with your own class that adds logging before calling the package’s handler. Prefer a wrapper over editing vendor files — archived packages should be treated as read-only.
Plan the exit. Write role checks behind a thin app-level interface (AuthorizationService::isAdmin($user)) so that when you outgrow the package — whether to native policies or a maintained RBAC library — you change the implementation in one place instead of grepping for hasRole across the codebase. For a package this small and unmaintained, that abstraction is the highest-value thing you can add on day one.
How can I help you explore Laravel packages today?