Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Simple Permissions Laravel Package

guava/simple-permissions

A lightweight permissions package for Laravel that lets you assign roles and permissions to users through a simple, readable API, with middleware and Blade directives for guarding routes and views without the overhead of larger ACL libraries.

View on GitHub
Deep Wiki
Context7

Getting Started

guavacz/simple-permissions is a lightweight, roles-only alternative to spatie/laravel-permission. It stores roles in a roles table and wires them to any Eloquent model through a polymorphic pivot (model_has_roles).

  1. Install the package (Laravel package discovery registers the service provider automatically):
composer require guavacz/simple-permissions
  1. Publish the config and migrations, then migrate:
php artisan vendor:publish --tag="simple-permissions-config"
php artisan vendor:publish --tag="simple-permissions-migrations"
php artisan migrate
  1. Add the HasRoles trait to the model(s) that should own roles:
use GuavaCZ\SimplePermissions\Traits\HasRoles;

class User extends Authenticatable
{
    use HasRoles;
}
  1. Seed roles once (they are not created on demand):
use GuavaCZ\SimplePermissions\Models\Role;

Role::firstOrCreate(['name' => 'admin']);
Role::firstOrCreate(['name' => 'editor']);
  1. First use case — assign and check:
$user->assignRole('admin');
$user->hasRole('admin');            // true
$user->assignRole(['admin', 'editor']);
$user->hasAnyRole(['editor']);       // true

Where to look first: config/simple-permissions.php (model + table overrides), GuavaCZ\SimplePermissions\Models\Role, and GuavaCZ\SimplePermissions\Traits\HasRoles. Those three files are the whole surface area.

Implementation Patterns

Assignment is a pivot write, not a string column. Treat roles as relational data:

$user->assignRole('admin');                 // string or Role instance
$user->assignRole(['admin', 'editor']);    // array
$user->removeRole('editor');
$user->syncRoles(['admin']);               // idempotent — best for seeders/jobs

syncRoles() is the safe choice for anything that may run twice (seeders, onboarding jobs, imports). assignRole() on an already-assigned role is harmless but syncRoles() makes intent explicit.

Reading roles:

$user->roles;                      // Collection of Role models
$user->roles->pluck('name');       // ['admin', 'editor']
$user->hasRole('admin');
$user->hasAnyRole(['admin', 'editor']);
$user->hasAllRoles(['admin', 'editor']);

Authorisation wiring. The package stops at roles — map them to abilities with Gates/Policies, which is the idiomatic Laravel path:

// AppServiceProvider::boot()
Gate::define('manage-users', fn (User $user) => $user->hasRole('admin'));

// Policy
public function update(User $user, Post $post): bool
{
    return $user->hasRole('editor') || $user->id === $post->user_id;
}

In Blade, just call the trait method (no directive is shipped):

@if(auth()->user()->hasRole('admin'))
    <a href="{{ route('admin.dashboard') }}">Admin</a>
@endif

Custom middleware for route protection:

class EnsureUserHasRole
{
    public function handle(Request $request, Closure $next, string ...$roles)
    {
        abort_unless(array_intersect($roles, $request->user()?->roles->pluck('name')->all() ?? []), 403);
        return $next($request);
    }
}

Eager loading. There is no built-in cache; hasRole() resolves against the loaded relation or queries. In list views, always:

$users = User::with('roles')->paginate();

and avoid per-row hasRole() calls on unloaded relations.

Custom Role model. Override via config, not by editing vendor files:

// config/simple-permissions.php
'models' => ['role' => App\Models\Role::class],

Your model must extend the package's Role (or match its contract) so the trait's relationships keep working.

Testing. Seed roles in a seeder or setUp, and use RefreshDatabase; assignments are plain pivot rows, so factories compose cleanly:

$user = User::factory()->create();
$user->assignRole('admin');

Gotchas and Tips

  • Upgrade from 1.1.0 to 1.1.1. The patch fixes a roles cast issue — if on 1.1.0 you saw $user->roles return an unexpected type (raw array/JSON or a non-collection after assignment or serialization), bump the constraint, composer update, and re-run php artisan config:cache/cache:clear if you cache config.
  • It is roles-only despite the package name. There is no per-permission table. Do not look for hasPermissionTo(); express abilities as Gates/Policies keyed off role names.
  • No implicit role creation. Passing an unknown name to assignRole() expects an existing role; seed with Role::firstOrCreate(['name' => '…']) before assigning, especially in fresh environments and CI.
  • Stale relations. After assignRole()/removeRole(), a previously loaded roles relation on the same instance can be stale. Call $user->refresh() or $user->load('roles') before re-checking in long-running jobs/tests.
  • No query cache. Unlike Spatie, role lookups hit the DB or the loaded relation. Eager-load roles, and if you have hot checks in a request, resolve once and reuse ($roles = $user->roles->pluck('name')->all()).
  • Morph map discipline. model_has_roles.model_type stores the model class (or morph alias). If you introduce Relation::enforceMorphMap(), existing rows must be backfilled or the checks silently return false — verify with a quick DB::table('model_has_roles')->get().
  • Guard names. Roles carry a guard_name; if you have multiple guards, create roles per guard and match the authenticated guard, or hasRole() will miss.
  • Deleting roles. Removing a Role row does not guarantee pivot cleanup unless your migration declares the FK cascade. Prefer syncRoles([]) on models, or verify the FK in the published migration before deleting.
  • Config caching. config/simple-permissions.php changes (custom model/table names) require php artisan config:clear/config:cache; table renames also require editing the published migration before the first migrate.
  • Debugging the relation. $user->roles()->toSql() and DB::listen() are the fastest way to confirm guard/model-type mismatches; the generated query shows the exact model_type and guard_name being filtered.
  • Extension point. Add domain methods on your own User/Role subclasses or a small service rather than forking the trait — the trait is intentionally minimal, and wrapping it keeps future patch upgrades (like 1.1.1) frictionless.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core