permissionService->userHas($actorUserId, PermissionService::ROLES_ASSIGN_ALL)) { return $this->roleRepository->listActiveIds(); } $actorRoleIds = $this->userRoleRepository->listRoleIdsByUserId($actorUserId); if (!$actorRoleIds) { return []; } return $this->assignableRoleRepository->listAssignableRoleIdsByRoleIds($actorRoleIds); } /** * Computes the effective role set respecting the actor's assignable scope. * * Roles the actor cannot assign are "frozen" — they remain as-is. * frozen = currentIds ∩ NOT-assignable * touched = submittedIds ∩ assignable * result = frozen ∪ touched * * @param int[] $submittedIds Role IDs the form submitted * @param int[] $currentIds Role IDs the target user currently has * @return int[] */ public function computeEffectiveRoleIds(int $actorUserId, array $submittedIds, array $currentIds): array { $assignable = $this->listAssignableRoleIdsForActor($actorUserId); $assignableMap = array_fill_keys($assignable, true); // Roles the actor cannot touch — keep them as they are. $frozen = array_values(array_filter( $currentIds, static fn (int $id): bool => !isset($assignableMap[$id]) )); // From the submitted set, only keep roles the actor is allowed to assign. $touched = array_values(array_filter( $submittedIds, static fn (int $id): bool => isset($assignableMap[$id]) )); return array_values(array_unique(array_merge($frozen, $touched))); } /** * Saves which roles this role can assign (admin UI) and logs the change. * * @param int[] $assignableRoleIds */ public function replaceAssignableRoles(int $roleId, array $assignableRoleIds, int $actorUserId): bool { $beforeIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId); $result = $this->assignableRoleRepository->replaceForRole($roleId, $assignableRoleIds); $afterIds = $this->assignableRoleRepository->listAssignableRoleIdsByRoleId($roleId); if ($beforeIds !== $afterIds) { $this->systemAuditService->record('admin.roles.assignable_roles.update', 'success', [ 'actor_user_id' => $actorUserId > 0 ? $actorUserId : null, 'target_type' => 'role', 'target_id' => $roleId, 'before' => ['assignable_role_ids' => $beforeIds], 'after' => ['assignable_role_ids' => $afterIds], ]); } return $result; } }