roleProvider->getRoles(); $rolesWithoutScopes = $this->roleProvider->getRolesWithoutScopes(); // Group roles by title $groups = []; foreach ($roles as $role) { $title = $this->roleProvider->getRoleTitle($role); $title ??= 'Other'; $groups[$title][] = $role; } // Sort groups by title ksort($groups, SORT_NATURAL | SORT_FLAG_CASE); $lines = []; foreach ($groups as $title => $roleList) { // Sort roles by translated label for deterministic output usort($roleList, function (string $a, string $b): int { $ta = $this->translator->trans($a); $tb = $this->translator->trans($b); return strcasecmp($ta, $tb); }); $translatedTitle = $this->translator->trans($title); $lines[] = '## '.$translatedTitle; foreach ($roleList as $role) { // Translate primary role $translatedRole = $this->translator->trans($role); // Scope marker: (S) if needs scope, (~~S~~) if no scope required $needsScope = !in_array($role, $rolesWithoutScopes, true); $scopeMarker = $needsScope ? '(S)' : '(~~S~~)'; // Compute dependent roles from hierarchy (exclude itself) $reachable = $this->roleHierarchy->getReachableRoleNames([$role]); $dependents = array_values(array_filter($reachable, static fn (string $r): bool => $r !== $role)); // Translate dependents and sort deterministically $translatedDependents = array_map(fn (string $r) => $this->translator->trans($r), $dependents); sort($translatedDependents, SORT_NATURAL | SORT_FLAG_CASE); if (count($translatedDependents) > 0) { $lines[] = sprintf('- **%s** %s: %s', $translatedRole, $scopeMarker, implode(', ', $translatedDependents)); } else { $lines[] = sprintf('- **%s** %s', $translatedRole, $scopeMarker); } } // Add a blank line between groups $lines[] = ''; } // Trim possible trailing blank line $markdown = rtrim(implode("\n", $lines)); return $markdown."\n"; // End with newline for POSIX friendliness } }