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

Code before:
    public function __construct(
        private readonly string $direction = self::DIRECTION_TOP_TO_BOTTOM,
    ) {
        if (!\in_array($direction, self::VALID_DIRECTIONS, true)) {
            throw new \InvalidArgumentException(\sprintf(
                'Direction "%s" is not valid, valid directions are: "%s".',
                $direction,
                implode('", "', self::VALID_DIRECTIONS)
            ));
        }
    }

    /**
     * Dumps the role hierarchy as a Mermaid flowchart.
     *
     * @param RoleHierarchyInterface $roleHierarchy The role hierarchy to dump
     */
    public function dump(RoleHierarchyInterface $roleHierarchy): string
    {
        $hierarchy = $this->extractHierarchy($roleHierarchy);

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

        $output = ["graph {$this->direction}"];
        $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
    {
        $reflection = new \ReflectionClass($roleHierarchy);

        if ($reflection->hasProperty('hierarchy')) {
            $hierarchyProperty = $reflection->getProperty('hierarchy');
            return $hierarchyProperty->getValue($roleHierarchy);
        }

        return [];

Review comment (suggestion):
If the RoleHierarchyInterface is not a child of RoleHierarchy, it could have a `hierarchy` property that has a totally unrelated type.

And if the RoleHierarchyInterface is a child of RoleHierarchy, this check will return `false` as the property is private: https://3v4l.org/jtqTG#vnull

You should check `$roleHierarchy instanceof RoleHierarchy` and then create the ReflectionClass for `RoleHierarchy::class`

Code after:

        $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}";
    }