php 45 lines · 8 steps

Idempotent role assignment in Laravel

A service that attaches department-scoped roles to a user with pivot metadata, without duplicating existing assignments.

Explained by highlit
1<?php
2 
3namespace App\Services;
4 
5use App\Models\Role;
6use App\Models\User;
7use Illuminate\Support\Collection;
8use Illuminate\Support\Facades\DB;
9 
10class RoleAssignmentService
11{
12 public function assignDepartmentRoles(User $user, array $roleSlugs, int $assignedBy): Collection
13 {
14 $roles = Role::query()
15 ->where('department_id', $user->department_id)
16 ->whereIn('slug', $roleSlugs)
17 ->get();
18 
19 if ($roles->isEmpty()) {
20 return collect();
21 }
22 
23 $now = now();
24 
25 $pivotData = $roles->mapWithKeys(fn (Role $role) => [
26 $role->getKey() => [
27 'assigned_by' => $assignedBy,
28 'assigned_at' => $now,
29 'source' => 'department',
30 ],
31 ])->all();
32 
33 $changes = DB::transaction(function () use ($user, $pivotData) {
34 return $user->roles()->syncWithoutDetaching($pivotData);
35 });
36 
37 $attached = $roles->whereIn('id', $changes['attached']);
38 
39 $attached->each(function (Role $role) use ($user) {
40 RoleAssigned::dispatch($user, $role);
41 });
42 
43 return $attached;
44 }
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Scoping the role query to the user's department prevents assigning roles that don't belong to their organizational unit.
  2. 2syncWithoutDetaching adds new pivot rows while leaving existing ones untouched, making the operation safe to retry.
  3. 3Firing events only for the roles actually attached avoids duplicate notifications on repeated calls.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent role assignment in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code