Gestion des rôles & permissions (Spatie)
Guide pratique et cohérent pour mettre en place et appliquer les rôles & permissions
avec spatie/laravel-permission dans les routes,
contrôleurs, Blade et Livewire.
Pré‑requis rapides
-
use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { use HasRoles; // Optionnel si multi-guard: // protected $guard_name = 'web'; } -
Purger le cache après seed/migration :
use Spatie\Permission\PermissionRegistrar; app()[PermissionRegistrar::class]->forgetCachedPermissions();À mettre en fin de seeder ou dans une commande artisan de maintenance.
-
Convention de nommage : resource.action (ex: users.view) ou action-resource (ex: view-users). Différencier own vs any si pertinent.
Assigner rôles & permissions
$user = User::find($id);
// Rôle(s)
$user->assignRole('admin'); // ajoute
$user->syncRoles(['admin','editor']); // remplace
// Permission(s) directe(s)
$user->givePermissionTo('manage-settings');
$user->syncPermissions(['view-users','edit-users']);
// Vérifier
$user->hasRole('admin'); // bool
$user->can('view-users'); // bool (hérite des rôles)
$user->hasAnyRole(['admin','manager']); // bool
Middleware dans les routes
Route::middleware(['auth','role:admin'])->group(function () {
Route::get('/admin', AdminController::class)->name('admin.dashboard');
});
Route::middleware(['auth','permission:view-users'])->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
});
Route::middleware(['auth','role_or_permission:admin|manage-settings'])->group(function () {
Route::get('/settings', SettingsController::class)->name('settings.index');
});
Avantage : logique d’accès proche de l’URL, lisible et robuste.
Contrôleurs
class UserController extends Controller
{
public function __construct()
{
$this->middleware(['permission:view-users'])->only('index','show');
$this->middleware(['permission:create-users'])->only('create','store');
$this->middleware(['permission:edit-users'])->only('edit','update');
$this->middleware(['permission:delete-users'])->only('destroy');
}
}
public function destroy(User $user)
{
$this->authorize('delete-users');
// ou bien
abort_unless(auth()->user()->can('delete-users'), 403);
$user->delete();
return back()->with('ok','Utilisateur supprimé.');
}
Blade
@role('admin')
<a href="{{ route('users.create') }}" class="btn">Créer un utilisateur</a>
@endrole
@hasanyrole('admin|manager')
<a href="{{ route('roles.index') }}" class="btn">Rôles</a>
@endhasanyrole
@can('edit-users')
<a href="{{ route('users.edit', $user) }}" class="btn">Éditer</a>
@endcan
@cannot('delete-users')
<span class="text-gray-400">Suppression non autorisée</span>
@endcannot
@canany(['view-users', 'view-own-appointments'])
<x-users.table :users="$users" />
@endcanany
Astuce UI : parfois afficher un bouton grisé avec un tooltip “Autorisation requise : edit-users”.
Livewire 3
class UsersTable extends \Livewire\Component
{
public function mount()
{
abort_unless(auth()->user()->can('view-users'), 403);
}
public function deleteUser(int $userId)
{
abort_unless(auth()->user()->can('delete-users'), 403);
User::findOrFail($userId)->delete();
$this->dispatch('userDeleted');
}
public function render()
{
$users = auth()->user()->can('view-users')
? User::query()->latest()->paginate(15)
: collect();
return view('livewire.users-table', compact('users'));
}
}
Policies & Gates
class UserPolicy
{
public function viewAny(User $actor): bool
{
return $actor->can('view-users');
}
public function view(User $actor, User $subject): bool
{
return $actor->can('view-users') || $actor->is($subject);
}
public function create(User $actor): bool
{
return $actor->can('create-users');
}
public function update(User $actor, User $subject): bool
{
return $actor->can('edit-users');
}
public function delete(User $actor, User $subject): bool
{
return $actor->can('delete-users');
}
}
// AuthServiceProvider
protected $policies = [
\App\Models\User::class => \App\Policies\UserPolicy::class,
];
// Usage
$this->authorize('update', $user);
// Blade: @can('delete', $user)
...
@endcan
Cas own vs any
// Policy pour Appointment
public function view(User $actor, Appointment $appt): bool
{
if ($actor->can('view-appointments')) { // any
return true;
}
if ($actor->can('view-own-appointments')) {
return $appt->user_id === $actor->id;
}
return false;
}
// Dans une requête
$query = Appointment::query();
if (!auth()->user()->can('view-appointments') && auth()->user()->can('view-own-appointments')) {
$query->where('user_id', auth()->id());
}
Bonnes pratiques
- Commencer large (view/create/edit/delete), affiner si besoin réel.
- Vérifier côté UI avec @can/@role mais centraliser la logique côté Policies/contrôleurs.
- Nommer de manière cohérente et invalider le cache après seed/déploiement.
- Multi‑guard : préciser guard_name sur Role/Permission/User si nécessaire.
Tests (Pest)
it('lets admin list users', function () {
$admin = User::factory()->create()->assignRole('admin');
actingAs($admin)
->get(route('users.index'))
->assertOk();
});
it('forbids regular user to delete users', function () {
$user = User::factory()->create()->assignRole('user');
$target = User::factory()->create();
actingAs($user)
->delete(route('users.destroy', $target))
->assertForbidden();
});
Seeder : micro‑ajustements
// Multi‑guard explicite
Permission::firstOrCreate(['name' => $permission, 'guard_name' => 'web']);
$role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => 'web']);
// Purge du cache en fin de seeder
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
$this->command->info('Rôles & permissions rafraîchis.');
Exemples rapides
// Menu admin
@can('access-admin')
<x-nav.link href="{{ route('admin.dashboard') }}">Admin</x-nav.link>
@endcan
// Route group admin
Route::prefix('admin')
->middleware(['auth','permission:access-admin'])
->group(function () {
Route::get('/', AdminController::class)->name('admin.dashboard');
Route::resource('users', UserController::class)->middleware('permission:view-users');
});
// Boutons d’action
@can('edit-users')
<a href="{{ route('users.edit', $user) }}" class="btn btn-sm">Éditer</a>
@endcan
@can('delete-users')
<form method="POST" action="{{ route('users.destroy', $user) }}" class="inline">
@csrf @method('DELETE')
<button class="btn btn-sm btn-danger" onclick="return confirm('Supprimer ?')">Supprimer</button>
</form>
@endcan
// Livewire – guard action
public function promoteToAdmin(int $id)
{
abort_unless(auth()->user()->can('edit-users'), 403);
$u = User::findOrFail($id);
$u->syncRoles(['admin']);
$this->dispatch('toast', body: 'Promu admin');
}
Règles claires, accès maîtrisé
Définissez des permissions lisibles, appliquez‑les partout (routes, contrôleurs, Policies, UI), et purgez le cache après déploiement pour garantir un comportement fiable.