Pragmatic Roles and Permissions in FilamentPHP

The Problem
When building a FilamentPHP application, you often need to manage different user roles with varying levels of access. In a restaurant management system, for example, you might have administrators who can do everything, office staff who manage orders, kitchen staff who view kitchen-related data, and dining room staff who need access to orders and some kitchen information.
Laravel's native Policies and Gates are powerful, but at times, we may desire a solution where we may change permissions at database level.
The Solution
From the popular packages available, there are some who tackle a complex approach for very granular permissions, but that may be overkill for many applications. We need something simple, maintainable, and easy to understand.
The solution involves three key components working together: Laravel Ladder for permission management, PHP Enums for type-safe role definitions, and Filament's built-in integration with Laravel's authorization system.
Ladder documentation at: https://github.com/eneadm/ladderStep 1: Define Roles as Enums
First, we create a clean, type-safe Role enum that implements Filament's HasLabel and HasColor interfaces. This
gives us autocomplete, validation, and beautiful badge displays:
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
use Illuminate\Contracts\Support\Htmlable;
enum Role: string implements HasLabel, HasColor
{
case Admin = 'admin';
case Office = 'office';
case Kitchen = 'kitchen';
case DiningRoom = 'dining_room';
public function getLabel(): string|Htmlable|null
{
return match ($this) {
self::Admin => 'Administrator',
self::Office => 'Office Area',
self::Kitchen => 'Kitchen Area',
self::DiningRoom => 'Dining Room Area',
};
}
public function getColor(): string|array|null
{
return match ($this) {
self::Admin => 'danger',
self::Office => 'info',
self::Kitchen => 'success',
self::DiningRoom => 'warning',
};
}
}
Step 2: Configure Permissions with Laravel Ladder
Next, we use Laravel Ladder to define what each role can do. The beauty of Ladder is its simple, declarative API. Create a service provider to configure your permissions:
<?php
namespace App\Providers;
use App\Enums\Role;
use Illuminate\Support\ServiceProvider;
use Ladder\Ladder;
class LadderServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->configurePermissions();
}
/**
* Configure the permissions that are available within the application.
*/
protected function configurePermissions(): void
{
Ladder::role(Role::Admin->value, 'Administrator', [
'*',
])->description('Administrator users can perform any action.');
Ladder::role(Role::DiningRoom->value, 'Dining Room Area', [
'orders:*',
'kitchen:read',
])->description('Editor users have the ability to read, create, and update.');
}
}
The permission syntax is intuitive: * means everything, orders:* means all order operations, and kitchen:read
means read-only access to kitchen data.
Step 3: Add the Roles feature to Your User Model
Simply add the HasRoles trait from Ladder to your User model:
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Ladder\HasRoles;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasRoles;
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
Step 4: User Roles management
Within your Filament user resource, you can now easily manage user roles. We'll have a Select field that allows
assigning multiple roles to a user, using our Role enum for options:
<?php
namespace App\Filament\Resources\Users\Schemas;
use App\Enums\Role;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Operation;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->readOnly(),
TextInput::make('email')
->label('Email address')
->readOnly(),
Select::make('roles')
->hiddenOn(Operation::Create)
->multiple()
->options(Role::class),
]);
}
}
The options(Role::class) automatically populates the select field with all our enum values:

Step 5: Roles persistence
We'll take care of persisting the roles when editing, and loading them as well.
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Enums\Role;
use App\Filament\Resources\Users\UserResource;
use App\Models\User;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use Ladder\Models\UserRole;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
/** @var User $this->record */
$userRoles = UserRole::query()
->where('user_id', $this->record->id)
->pluck('role')
->toArray();
$data['roles'] = $userRoles;
return $data;
}
public function handleRecordUpdate(Model $record, array $data): Model
{
$rolesToPersist = fluent($data)
->collect('roles');
// Delete Roles that are not in the form data
/** @var User $record */
UserRole::query()
->where('user_id', $record->id)
->whereNotIn('role', $rolesToPersist->toArray())
->delete();
// Add new roles, ignoring the ones that already exist
$rolesToPersist
->each(fn(Role $role) => $record->roles()->firstOrCreate([
'role' => $role,
]));
return $record;
}
}
Step 6: Display Roles Beautifully in the View
Create an infolist to display user information with their assigned roles as colored badges:
<?php
namespace App\Filament\Resources\Users\Schemas;
use App\Enums\Role;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class UserInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->columns(1)
->components([
Section::make('User Information')
->description('Basic user details and contact information')
->icon('heroicon-o-user')
->columns(2)
->schema([
TextEntry::make('name')
->label('Full Name')
->icon('heroicon-m-user')
->weight('medium')
->copyable()
->size('lg'),
TextEntry::make('email')
->label('Email Address')
->icon('heroicon-m-envelope')
->copyable()
->copyMessage('Email address copied!')
->color('gray'),
TextEntry::make('roles')
->label('Assigned Roles')
->formatStateUsing(fn ($state): string => Role::tryFrom($state->role)->getLabel())
->badge()
// We have to calculate the color because this comes from a relationship, not an Enum cast
->color(fn ($state): string => Role::tryFrom($state->role)?->getColor())
->icon('heroicon-m-shield-check'),
IconEntry::make('email_verified_at')
->label('Email Verified')
->boolean()
->trueIcon('heroicon-o-check-badge')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger'),
]),
Section::make('Account Timestamps')
->description('Account creation and modification history')
->icon('heroicon-o-clock')
->columns()
->schema([
TextEntry::make('email_verified_at')
->label('Email Verified At')
->placeholder('Not verified')
->icon('heroicon-m-envelope-open')
->color(fn ($state) => $state ? 'success' : 'warning'),
TextEntry::make('updated_at')
->columnStart(1)
->label('Last Updated')
->icon('heroicon-m-pencil-square')
->since()
->color('gray')
->placeholder('Never updated'),
TextEntry::make('created_at')
->label('Account Created')
->icon('heroicon-m-plus-circle')
->since()
->placeholder('N/A'),
]),
]);
}
}
Step 7: Display Roles Beautifully in the Table
Let's use convenient badges to show user roles in the users table:
<?php
namespace App\Filament\Resources\Users\Tables;
use App\Enums\Role;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('email')
->label('Email address')
->searchable(),
TextColumn::make('roles')
->badge()
// We have to calculate the color because this comes from a relationship, not an Enum cast
->color(fn ($state): string => Role::tryFrom($state->role)?->getColor())
->formatStateUsing(fn ($state): string => Role::tryFrom($state->role)->getLabel()),
TextColumn::make('email_verified_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
ViewAction::make(),
EditAction::make(),
]);
}
}

With this setup, we can have a sensible extensible roles and permissions system in our FilamentPHP application. We simply just add a role or more to the user, and the permissions are automatically applied based on our Ladder configuration. It's more than enough for plenty of applications, and it's easy to maintain and understand.