Repository: symfony/symfony
File: src/Symfony/Component/Security/Core/Dumper/MermaidDumper.php (PHP)

Code before:
    {
        $allRoles = [];

        foreach ($hierarchy as $parentRole => $childRoles) {
            $allRoles[] = $parentRole;
            foreach ($childRoles as $childRole) {
                $allRoles[] = $childRole;
            }
        }

        return array_unique($allRoles);
    }

    /**
     * Formats a role node for Mermaid.
     */
    private function formatRoleNode(string $role): string
    {
        $escapedRole = $this->escapeRoleName($role);
        return "    {$escapedRole}";
    }

    /**
     * Escapes role name for use as Mermaid node ID.
     */
    private function escapeRoleName(string $role): ?string
    {
        // Replace any non-alphanumeric characters with underscores
        return preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
    }
}


Review comment (suggestion):
I'd call it `normalizeRoleName`, because there's no actual escaping here

Code after:
<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Security\Core\Dumper;

use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;

/**
 * MermaidDumper dumps a Mermaid flowchart describing role hierarchy.
 *
 * @author Damien Fernandes <damien.fernandes24@gmail.com>
 */
class MermaidDumper
{
    /**
     * Dumps the role hierarchy as a Mermaid flowchart.
     *
     * @param RoleHierarchyInterface $roleHierarchy The role hierarchy to dump
     * @param MermaidDirectionEnum   $direction     The direction of the flowchart
     */
    public function dump(RoleHierarchyInterface $roleHierarchy, MermaidDirectionEnum $direction = MermaidDirectionEnum::TOP_TO_BOTTOM): string
    {
        $hierarchy = $this->extractHierarchy($roleHierarchy);

        if (!$hierarchy) {
            return "graph {$direction->value}\n    classDef default fill:#e1f5fe;";
        }

        $output = ["graph {$direction->value}"];
        $allRoles = $this->getAllRoles($hierarchy);

        foreach ($allRoles as $role) {
            $output[] = $this->formatRoleNode($role);
        }

        foreach ($hierarchy as $parentRole => $childRoles) {
            foreach ($childRoles as $childRole) {
                $output[] = "    {$this->normalizeRoleName($parentRole)} --> {$this->normalizeRoleName($childRole)}";
            }
        }

        return implode("\n", array_filter($output));
    }

    private function extractHierarchy(RoleHierarchyInterface $roleHierarchy): array
    {
        if (!$roleHierarchy instanceof RoleHierarchy) {
            return [];
        }

        $reflection = new \ReflectionClass(RoleHierarchy::class);

        $hierarchyProperty = $reflection->getProperty('hierarchy');

        return $hierarchyProperty->getValue($roleHierarchy);
    }

    private function getAllRoles(array $hierarchy): array
    {
        $allRoles = [];

        foreach ($hierarchy as $parentRole => $childRoles) {
            $allRoles[] = $parentRole;
            foreach ($childRoles as $childRole) {
                $allRoles[] = $childRole;
            }
        }

        return array_unique($allRoles);
    }

    private function formatRoleNode(string $role): string
    {
        $escapedRole = $this->normalizeRoleName($role);

        return "    {$escapedRole}";
    }

    /**
     * Normalizes the role name by replacing non-alphanumeric characters with underscores.
     */
    private function normalizeRoleName(string $role): ?string
    {
        return preg_replace('/[^a-zA-Z0-9_]/', '_', $role);
    }
}